diff --git a/doc/api/embed_parameters.md b/doc/api/embed_parameters.md index 79b60f2140e..d6f27af0556 100644 --- a/doc/api/embed_parameters.md +++ b/doc/api/embed_parameters.md @@ -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. ``` - + ``` ## showLineNumbers @@ -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 + diff --git a/src/static/css/iframe_editor.css b/src/static/css/iframe_editor.css index 7267375a473..42951f486b7 100644 --- a/src/static/css/iframe_editor.css +++ b/src/static/css/iframe_editor.css @@ -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; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index fc339ab7864..34b7e79a1ec 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -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 () { diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index b7b94f720c6..70afc0e09d2 100644 --- a/src/static/js/pad_editor.js +++ b/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. @@ -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); } }; @@ -55,7 +63,6 @@ const padeditor = (() => { } self.initViewOptions(); self.setViewOptions(initialViewOptions); - // view bar $('#viewbarcontents').show(); }, @@ -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 s // also, a value which has been set by the user will be not overwritten @@ -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 +}; diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js index b49d32eb8a1..37c5af3b11a 100644 --- a/tests/frontend/helper.js +++ b/tests/frontend/helper.js @@ -1,4 +1,5 @@ -var helper = {}; +'use strict'; +const helper = {}; // eslint-disable-line (function () { let $iframe; const @@ -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; @@ -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 @@ -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 @@ -112,8 +118,7 @@ var helper = {}; } if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`; - $iframe = $(``); - + $iframe = $(``); // needed for retry const origPadName = padName; @@ -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"]')); @@ -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; @@ -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; }); @@ -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 (
). // Use beginning of line textNodeWhereOffsetIs = $targetLine.get(0); diff --git a/tests/frontend/specs/scrollTo.js b/tests/frontend/specs/scrollTo.js new file mode 100755 index 00000000000..47fe1ca7ef0 --- /dev/null +++ b/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. + }); + }); +});