diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 4aaedaf0e..1fdf7cd6c 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -20,13 +20,15 @@ * limitations under the License. */ +'use strict'; + const Security = require('./security'); /** - * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + * Generates a random String with the given length. Is needed to generate the Author, Group, + * readonly, session Ids */ - -function randomString(len) { +const randomString = (len) => { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let randomstring = ''; len = len || 20; @@ -35,41 +37,69 @@ function randomString(len) { randomstring += chars.substring(rnum, rnum + 1); } return randomstring; -} +}; -var padutils = { - escapeHtml(x) { - return Security.escapeHTML(String(x)); - }, - uniqueId() { +const padutils = { + escapeHtml: (x) => Security.escapeHTML(String(x)), + uniqueId: () => { const pad = require('./pad').pad; // Sidestep circular dependency - function encodeNum(n, width) { - // returns string that is exactly 'width' chars, padding with zeros - // and taking rightmost digits - return (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width); - } - return [pad.getClientIp(), encodeNum(+new Date(), 7), encodeNum(Math.floor(Math.random() * 1e9), 4)].join('.'); + // returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits + const encodeNum = + (n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width); + return [ + pad.getClientIp(), + encodeNum(+new Date(), 7), + encodeNum(Math.floor(Math.random() * 1e9), 4), + ].join('.'); }, // e.g. "Thu Jun 18 2009 13:09" - simpleDateTime(date) { + simpleDateTime: (date) => { const d = new Date(+date); // accept either number or date const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()]; - const month = (['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[d.getMonth()]; + const month = ([ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ])[d.getMonth()]; const dayOfMonth = d.getDate(); const year = d.getFullYear(); const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`; return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`; }, - findURLs(text) { + findURLs: (text) => { // copied from ACE - const _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; - const _REGEX_URLCHAR = new RegExp(`(${/[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source}|${_REGEX_WORDCHAR.source})`); - const _REGEX_URL = new RegExp(`${/(?:(?:https?|s?ftp|ftps|file|nfs):\/\/|(about|geo|mailto|tel):)/.source + _REGEX_URLCHAR.source}*(?![:.,;])${_REGEX_URLCHAR.source}`, 'g'); + const _REGEX_WORDCHAR = new RegExp(`[${[ + '\u0030-\u0039', + '\u0041-\u005A', + '\u0061-\u007A', + '\u00C0-\u00D6', + '\u00D8-\u00F6', + '\u00F8-\u00FF', + '\u0100-\u1FFF', + '\u3040-\u9FFF', + '\uF900-\uFDFF', + '\uFE70-\uFEFE', + '\uFF10-\uFF19', + '\uFF21-\uFF3A', + '\uFF41-\uFF5A', + '\uFF66-\uFFDC', + ].join('')}]`); + const _REGEX_URLCHAR = new RegExp(`([-:@a-zA-Z0-9_.,~%+/?=&#;()$]|${_REGEX_WORDCHAR.source})`); + const _REGEX_URL = new RegExp( + '(?:(?:https?|s?ftp|ftps|file|nfs)://|(about|geo|mailto|tel):)' + + `${_REGEX_URLCHAR.source}*(?![:.,;])${_REGEX_URLCHAR.source}`, 'g'); // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] - - - function _findURLs(text) { + const _findURLs = (text) => { _REGEX_URL.lastIndex = 0; let urls = null; let execResult; @@ -81,34 +111,39 @@ var padutils = { } return urls; - } + }; return _findURLs(text); }, - escapeHtmlWithClickableLinks(text, target) { + escapeHtmlWithClickableLinks: (text, target) => { let idx = 0; const pieces = []; const urls = padutils.findURLs(text); - function advanceTo(i) { + const advanceTo = (i) => { if (i > idx) { pieces.push(Security.escapeHTML(text.substring(idx, i))); idx = i; } - } + }; if (urls) { for (let j = 0; j < urls.length; j++) { const startIndex = urls[j][0]; const href = urls[j][1]; advanceTo(startIndex); - // Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in the document. - // Not all browsers understand this attribute, but it's part of the HTML5 standard. - // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer + // Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in + // the document. Not all browsers understand this attribute, but it's part of the HTML5 + // standard. https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer // Additionally, we do rel="noopener" to ensure a higher level of referrer security. // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - pieces.push(''); + pieces.push( + ''); advanceTo(startIndex + href.length); pieces.push(''); } @@ -116,14 +151,14 @@ var padutils = { advanceTo(text.length); return pieces.join(''); }, - bindEnterAndEscape(node, onEnter, onEscape) { - // Use keypress instead of keyup in bindEnterAndEscape - // Keyup event is fired on enter in IME (Input Method Editor), But - // keypress is not. So, I changed to use keypress instead of keyup. - // It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0). + bindEnterAndEscape: (node, onEnter, onEscape) => { + // Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME + // (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup. + // It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox + // 3.6.10, Chrome 6.0.472, Safari 5.0). if (onEnter) { node.keypress((evt) => { - if (evt.which == 13) { + if (evt.which === 13) { onEnter(evt); } }); @@ -131,18 +166,18 @@ var padutils = { if (onEscape) { node.keydown((evt) => { - if (evt.which == 27) { + if (evt.which === 27) { onEscape(evt); } }); } }, - timediff(d) { + timediff: (d) => { const pad = require('./pad').pad; // Sidestep circular dependency - function format(n, word) { + const format = (n, word) => { n = Math.round(n); - return (`${n} ${word}${n != 1 ? 's' : ''} ago`); - } + return (`${n} ${word}${n !== 1 ? 's' : ''} ago`); + }; d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000); if (d < 60) { return format(d, 'second'); @@ -158,14 +193,14 @@ var padutils = { d /= 24; return format(d, 'day'); }, - makeAnimationScheduler(funcToAnimateOneStep, stepTime, stepsAtOnce) { + makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => { if (stepsAtOnce === undefined) { stepsAtOnce = 1; } let animationTimer = null; - function scheduleAnimation() { + const scheduleAnimation = () => { if (!animationTimer) { animationTimer = window.setTimeout(() => { animationTimer = null; @@ -181,43 +216,16 @@ var padutils = { } }, stepTime * stepsAtOnce); } - } - return { - scheduleAnimation, }; + return {scheduleAnimation}; }, - makeShowHideAnimator(funcToArriveAtState, initiallyShown, fps, totalMs) { + makeShowHideAnimator: (funcToArriveAtState, initiallyShown, fps, totalMs) => { let animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out const animationFrameDelay = 1000 / fps; const animationStep = animationFrameDelay / totalMs; - const scheduleAnimation = padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation; - - function doShow() { - animationState = -1; - funcToArriveAtState(animationState); - scheduleAnimation(); - } - - function doQuickShow() { // start showing without losing any fade-in progress - if (animationState < -1) { - animationState = -1; - } else if (animationState > 0) { - animationState = Math.max(-1, Math.min(0, -animationState)); - } - funcToArriveAtState(animationState); - scheduleAnimation(); - } - - function doHide() { - if (animationState >= -1 && animationState <= 0) { - animationState = 1e-6; - scheduleAnimation(); - } - } - - function animateOneStep() { - if (animationState < -1 || animationState == 0) { + const animateOneStep = () => { + if (animationState < -1 || animationState === 0) { return false; } else if (animationState < 0) { // animate show @@ -243,17 +251,37 @@ var padutils = { return true; } } - } + }; + + const scheduleAnimation = + padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation; return { - show: doShow, - hide: doHide, - quickShow: doQuickShow, + show: () => { + animationState = -1; + funcToArriveAtState(animationState); + scheduleAnimation(); + }, + quickShow: () => { // start showing without losing any fade-in progress + if (animationState < -1) { + animationState = -1; + } else if (animationState > 0) { + animationState = Math.max(-1, Math.min(0, -animationState)); + } + funcToArriveAtState(animationState); + scheduleAnimation(); + }, + hide: () => { + if (animationState >= -1 && animationState <= 0) { + animationState = 1e-6; + scheduleAnimation(); + } + }, }; }, _nextActionId: 1, uncanceledActions: {}, - getCancellableAction(actionType, actionFunc) { + getCancellableAction: (actionType, actionFunc) => { let o = padutils.uncanceledActions[actionType]; if (!o) { o = {}; @@ -261,27 +289,27 @@ var padutils = { } const actionId = (padutils._nextActionId++); o[actionId] = true; - return function () { + return () => { const p = padutils.uncanceledActions[actionType]; if (p && p[actionId]) { actionFunc(); } }; }, - cancelActions(actionType) { + cancelActions: (actionType) => { const o = padutils.uncanceledActions[actionType]; if (o) { // clear it delete padutils.uncanceledActions[actionType]; } }, - makeFieldLabeledWhenEmpty(field, labelText) { + makeFieldLabeledWhenEmpty: (field, labelText) => { field = $(field); - function clear() { + const clear = () => { field.addClass('editempty'); field.val(labelText); - } + }; field.focus(() => { if (field.hasClass('editempty')) { field.val(''); @@ -297,34 +325,28 @@ var padutils = { clear, }; }, - getCheckbox(node) { - return $(node).is(':checked'); - }, - setCheckbox(node, value) { + getCheckbox: (node) => $(node).is(':checked'), + setCheckbox: (node, value) => { if (value) { $(node).attr('checked', 'checked'); } else { $(node).removeAttr('checked'); } }, - bindCheckboxChange(node, func) { + bindCheckboxChange: (node, func) => { $(node).change(func); }, - encodeUserId(userId) { - return userId.replace(/[^a-y0-9]/g, (c) => { - if (c == '.') return '-'; - return `z${c.charCodeAt(0)}z`; - }); - }, - decodeUserId(encodedUserId) { - return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { - if (cc == '-') { return '.'; } else if (cc.charAt(0) == 'z') { - return String.fromCharCode(Number(cc.slice(1, -1))); - } else { - return cc; - } - }); - }, + encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => { + if (c === '.') return '-'; + return `z${c.charCodeAt(0)}z`; + }), + decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { + if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') { + return String.fromCharCode(Number(cc.slice(1, -1))); + } else { + return cc; + } + }), }; let globalExceptionHandler = null; @@ -402,13 +424,13 @@ padutils.setupGlobalExceptionHandler = () => { padutils.binarySearch = require('./ace2_common').binarySearch; // https://stackoverflow.com/a/42660748 -function inThirdPartyIframe() { +const inThirdPartyIframe = () => { try { return (!window.top.location.hostname); } catch (e) { return true; } -} +}; // This file is included from Node so that it can reuse randomString, but Node doesn't have a global // window object. diff --git a/tests/frontend/helper/methods.js b/tests/frontend/helper/methods.js index 191202c65..39245ea63 100644 --- a/tests/frontend/helper/methods.js +++ b/tests/frontend/helper/methods.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Spys on socket.io messages and saves them into several arrays * that are visible in tests diff --git a/tests/frontend/specs/timeslider_follow.js b/tests/frontend/specs/timeslider_follow.js index c570cbd07..be9bc8125 100644 --- a/tests/frontend/specs/timeslider_follow.js +++ b/tests/frontend/specs/timeslider_follow.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider follow', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -23,7 +25,7 @@ describe('timeslider follow', function () { helper.contentWindow().$('#playpause_button_icon').click(); let newTop; - return helper.waitForPromise(() => { + await helper.waitForPromise(() => { newTop = helper.contentWindow().$('#innerdocbody').offset(); return newTop.top < originalTop.top; }); @@ -96,9 +98,9 @@ describe('timeslider follow', function () { * @param {number} lineNum * @returns {boolean} scrolled to the lineOffset? */ -function hasFollowedToLine(lineNum) { +const hasFollowedToLine = (lineNum) => { const scrollPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop; - const lineOffset = helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop; - + const lineOffset = + helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop; return Math.abs(scrollPosition - lineOffset) < 1; -} +};