Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Scroll to Line number based on Hash IE http://foo.com/p/bar#L10 will scroll to line 10. #4554

Merged
merged 13 commits into from Dec 26, 2020
11 changes: 9 additions & 2 deletions doc/api/embed_parameters.md
Expand Up @@ -3,10 +3,10 @@ You can easily embed your etherpad-lite into any webpage by using iframes. You c

Example:

Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers.
Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers and will auto-focus on Line 4.

```
<iframe src='http://pad.test.de/p/PAD_NAME?showChat=false&showLineNumbers=false' width=600 height=400></iframe>
<iframe src='http://pad.test.de/p/PAD_NAME#L4?showChat=false&showLineNumbers=false' width=600 height=400></iframe>
```

## showLineNumbers
Expand Down Expand Up @@ -66,3 +66,10 @@ Example: `lang=ar` (translates the interface into Arabic)
Default: true
Displays pad text from right to left.

## #L
* Int

Default: 0
Focuses pad at specific line number and places caret at beginning of this line
Special note: Is not a URL parameter but instead of a Hash value

1 change: 1 addition & 0 deletions src/static/css/iframe_editor.css
Expand Up @@ -101,6 +101,7 @@ body.mozilla, body.safari {
font-size: 9px;
padding: 0 14px 0 10px;
font-family: monospace;
cursor: pointer;
}
.plugin-ep_author_neat #sidedivinner.authorColors .line-number {
padding-right: 10px;
Expand Down
1 change: 1 addition & 0 deletions src/static/js/ace2_inner.js
Expand Up @@ -676,6 +676,7 @@ function Ace2Inner() {
editorInfo.ace_doReturnKey = doReturnKey;
editorInfo.ace_isBlockElement = isBlockElement;
editorInfo.ace_getLineListType = getLineListType;
editorInfo.ace_setSelection = setSelection;

editorInfo.ace_callWithAce = function (fn, callStack, normalize) {
let wrapper = function () {
Expand Down
59 changes: 57 additions & 2 deletions src/static/js/pad_editor.js
@@ -1,5 +1,4 @@
'use strict';

/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
Expand Down Expand Up @@ -44,6 +43,15 @@ const padeditor = (() => {
$('#editorloadingbox').hide();
if (readyFunc) {
readyFunc();

// Listen for clicks on sidediv items
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
const targetLineNumber = $(this).index() + 1;
window.location.hash = `L${targetLineNumber}`;
});

exports.focusOnLine(self.ace);
}
};

Expand All @@ -55,7 +63,6 @@ const padeditor = (() => {
}
self.initViewOptions();
self.setViewOptions(initialViewOptions);

// view bar
$('#viewbarcontents').show();
},
Expand Down Expand Up @@ -89,6 +96,7 @@ const padeditor = (() => {
html10n.bind('localized', () => {
$('#languagemenu').val(html10n.getLanguage());
// translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist

// this does not interfere with html10n's normal value-setting because
// html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten
Expand Down Expand Up @@ -166,3 +174,50 @@ const padeditor = (() => {
})();

exports.padeditor = padeditor;

exports.focusOnLine = (ace) => {
// If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1);
if (lineNumber) {
if (lineNumber[0] === 'L') {
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
const lineNumberInt = parseInt(lineNumber.substr(1));
if (lineNumberInt) {
const $inner = $('iframe[name="ace_outer"]').contents().find('iframe')
.contents().find('#innerdocbody');
const line = $inner.find(`div:nth-child(${lineNumberInt})`);
if (line.length !== 0) {
let offsetTop = line.offset().top;
offsetTop += parseInt($outerdoc.css('padding-top').replace('px', ''));
const hasMobileLayout = $('body').hasClass('mobile-layout');
if (!hasMobileLayout) {
offsetTop += parseInt($inner.css('padding-top').replace('px', ''));
}
const $outerdocHTML = $('iframe[name="ace_outer"]').contents()
.find('#outerdocbody').parent();
$outerdoc.css({top: `${offsetTop}px`}); // Chrome
$outerdocHTML.animate({scrollTop: offsetTop}); // needed for FF
const node = line[0];
ace.callWithAce((ace) => {
const selection = {
startPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
endPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
};
ace.ace_setSelection(selection);
});
}
}
}
}
// End of setSelection / set Y position of editor
};
28 changes: 17 additions & 11 deletions tests/frontend/helper.js
@@ -1,4 +1,5 @@
var helper = {};
'use strict';
const helper = {}; // eslint-disable-line

(function () {
let $iframe; const
Expand Down Expand Up @@ -29,10 +30,9 @@ var helper = {};

const getFrameJQuery = function ($iframe) {
/*
I tried over 9000 ways to inject javascript into iframes.
I tried over 9001 ways to inject javascript into iframes.
This is the only way I found that worked in IE 7+8+9, FF and Chrome
*/

const win = $iframe[0].contentWindow;
const doc = win.document;

Expand Down Expand Up @@ -68,7 +68,8 @@ var helper = {};
// I don't fully understand it, but this function seems to properly simulate
// padCookie.setPref in the client code
helper.setPadPrefCookie = function (prefs) {
helper.padChrome$.document.cookie = (`prefsHttp=${escape(JSON.stringify(prefs))};expires=Thu, 01 Jan 3000 00:00:00 GMT`);
helper.padChrome$.document.cookie =
(`prefsHttp=${escape(JSON.stringify(prefs))};expires=Thu, 01 Jan 3000 00:00:00 GMT`);
};

// Functionality for knowing what key event type is required for tests
Expand Down Expand Up @@ -102,8 +103,13 @@ var helper = {};
}

// if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah.
let encodedParams;
if (opts.params) {
var encodedParams = `?${$.param(opts.params)}`;
encodedParams = `?${$.param(opts.params)}`;
}
let hash;
if (opts.hash) {
hash = `#${opts.hash}`;
}

// clear cookies
Expand All @@ -112,8 +118,7 @@ var helper = {};
}

if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`;
$iframe = $(`<iframe src='/p/${padName}${encodedParams || ''}'></iframe>`);

$iframe = $(`<iframe src='/p/${padName}${hash || ''}${encodedParams || ''}'></iframe>`);
// needed for retry
const origPadName = padName;

Expand All @@ -132,7 +137,8 @@ var helper = {};
if (opts.padPrefs) {
helper.setPadPrefCookie(opts.padPrefs);
}
helper.waitFor(() => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000).done(() => {
helper.waitFor(() => !$iframe.contents().find('#editorloadingbox')
.is(':visible'), 10000).done(() => {
helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'));
helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'));

Expand Down Expand Up @@ -175,7 +181,7 @@ var helper = {};
};

helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) {
const deferred = $.Deferred();
const deferred = $.Deferred(); // eslint-disable-line

const _fail = deferred.fail.bind(deferred);
let listenForFail = false;
Expand Down Expand Up @@ -245,7 +251,7 @@ var helper = {};
selection.addRange(range);
};

var getTextNodeAndOffsetOf = function ($targetLine, targetOffsetAtLine) {
const getTextNodeAndOffsetOf = function ($targetLine, targetOffsetAtLine) {
const $textNodes = $targetLine.find('*').contents().filter(function () {
return this.nodeType === Node.TEXT_NODE;
});
Expand All @@ -268,7 +274,7 @@ var helper = {};
});

// edge cases
if (textNodeWhereOffsetIs === null) {
if (textNodeWhereOffsetIs == null) {
// there was no text node inside $targetLine, so it is an empty line (<br>).
// Use beginning of line
textNodeWhereOffsetIs = $targetLine.get(0);
Expand Down
43 changes: 43 additions & 0 deletions tests/frontend/specs/scrollTo.js
@@ -0,0 +1,43 @@
'use strict';

describe('scrolls to line', function () {
// create a new pad with URL hash set before each test run
beforeEach(function (cb) {
helper.newPad({
hash: 'L4',
cb,
});
this.timeout(10000);
});

it('Scrolls down to Line 4', async function () {
this.timeout(10000);
const chrome$ = helper.padChrome$;
await helper.waitForPromise(() => {
const topOffset = parseInt(chrome$('iframe').first('iframe')
.contents().find('#outerdocbody').css('top'));
return (topOffset >= 100);
});
});
});

describe('doesnt break on weird hash input', function () {
// create a new pad with URL hash set before each test run
beforeEach(function (cb) {
helper.newPad({
hash: '#DEEZ123123NUTS',
cb,
});
this.timeout(10000);
});

it('Does NOT change scroll', async function () {
this.timeout(10000);
const chrome$ = helper.padChrome$;
await helper.waitForPromise(() => {
const topOffset = parseInt(chrome$('iframe').first('iframe')
.contents().find('#outerdocbody').css('top'));
return (!topOffset); // no css top should be set.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above, also applies to here

});
});
});