mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-22 16:36:15 -04:00
Feat/bundle js (#6511)
* Added minify * Added POC for browser * Moved first js files to ts * Fixed caret positioning * Added support for plugins * Fixed get undefined. * Removed require of socketio, l10n, html10n and error reporter * Added minify * Added POC for browser * Moved first js files to ts * Fixed caret positioning * Added support for plugins * Fixed get undefined. * Removed require of socketio, l10n, html10n and error reporter * Fixed popup not showing * Fixed timeslider * Reworked paths * Fixed loading * Don't generate sources map in production mode * Non working hmr * Added live reloading. * Fixed timeslider when hot reloading * Removed eval * Fixed. * Fixed env * Fixed frontend tests. * Added minifying via lightningcss * Added minify via esbuild * Fixed diagnostic url * Removed lightningcss * Fixed types * Fixed alias * Fixed loadtest * Fixed * Fixed loading ep_font_color3 * Restructure windows build * Fixed windows build * Fixed pnpm lock --------- Co-authored-by: SamTv12345 <samtv12345@samtv12345.com>
This commit is contained in:
parent
33b388b14c
commit
d6d636955c
53 changed files with 2764 additions and 1763 deletions
|
@ -4,7 +4,7 @@ const AttributeMap = require('./AttributeMap');
|
|||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const attributes = require('./attributes');
|
||||
const _ = require('./underscore');
|
||||
const underscore = require("underscore")
|
||||
|
||||
const lineMarkerAttribute = 'lmkr';
|
||||
|
||||
|
@ -45,7 +45,7 @@ const AttributeManager = function (rep, applyChangesetCallback) {
|
|||
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
|
||||
AttributeManager.lineAttributes = lineAttributes;
|
||||
|
||||
AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
||||
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
|
||||
|
||||
applyChangeset(changeset) {
|
||||
if (!this.applyChangesetCallback) return changeset;
|
||||
|
@ -335,7 +335,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
|
|||
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
|
||||
const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1])
|
||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
|
||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||
|
|
|
@ -27,9 +27,10 @@
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
const pluginUtils = require('./pluginfw/shared');
|
||||
|
||||
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
|
||||
const debugLog = (...args) => {};
|
||||
|
||||
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
|
||||
const rJQuery = require('ep_etherpad-lite/static/js/rjquery')
|
||||
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
||||
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
||||
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||
|
@ -257,19 +258,19 @@ const Ace2Editor = function () {
|
|||
|
||||
// <head> tag
|
||||
addStyleTagsFor(innerDocument, includedCSS);
|
||||
const requireKernel = innerDocument.createElement('script');
|
||||
requireKernel.type = 'text/javascript';
|
||||
requireKernel.src =
|
||||
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||
innerDocument.head.appendChild(requireKernel);
|
||||
//const requireKernel = innerDocument.createElement('script');
|
||||
//requireKernel.type = 'text/javascript';
|
||||
//requireKernel.src =
|
||||
// absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||
//innerDocument.head.appendChild(requireKernel);
|
||||
// Pre-fetch modules to improve load performance.
|
||||
for (const module of ['ace2_inner', 'ace2_common']) {
|
||||
/*for (const module of ['ace2_inner', 'ace2_common']) {
|
||||
const script = innerDocument.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
|
||||
`?callback=require.define&v=${clientVars.randomVersionString}`);
|
||||
innerDocument.head.appendChild(script);
|
||||
}
|
||||
}*/
|
||||
const innerStyle = innerDocument.createElement('style');
|
||||
innerStyle.type = 'text/css';
|
||||
innerStyle.title = 'dynamicsyntax';
|
||||
|
@ -284,7 +285,7 @@ const Ace2Editor = function () {
|
|||
innerDocument.body.classList.add('innerdocbody');
|
||||
innerDocument.body.setAttribute('spellcheck', 'false');
|
||||
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
|
||||
|
||||
/*
|
||||
debugLog('Ace2Editor.init() waiting for require kernel load');
|
||||
await eventFired(requireKernel, 'load');
|
||||
debugLog('Ace2Editor.init() require kernel loaded');
|
||||
|
@ -292,17 +293,16 @@ const Ace2Editor = function () {
|
|||
require.setRootURI(absUrl('../javascripts/src'));
|
||||
require.setLibraryURI(absUrl('../javascripts/lib'));
|
||||
require.setGlobalKeyPath('require');
|
||||
|
||||
*/
|
||||
// intentially moved before requiring client_plugins to save a 307
|
||||
innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
|
||||
innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||
innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
|
||||
innerWindow.Ace2Inner = ace2_inner;
|
||||
innerWindow.plugins = cl_plugins;
|
||||
|
||||
innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for plugins');
|
||||
await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));
|
||||
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));*/
|
||||
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||
await innerWindow.Ace2Inner.init(info, {
|
||||
inner: makeCSSManager(innerStyle.sheet),
|
||||
|
|
|
@ -30,6 +30,8 @@ const setAssoc = Ace2Common.setAssoc;
|
|||
const noop = Ace2Common.noop;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import Scroll from './scroll'
|
||||
|
||||
function Ace2Inner(editorInfo, cssManagers) {
|
||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
|
@ -42,7 +44,6 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const SkipList = require('./skiplist');
|
||||
const undoModule = require('./undomodule').undoModule;
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
const Scroll = require('./scroll');
|
||||
const DEBUG = false;
|
||||
|
||||
const THE_TAB = ' '; // 4
|
||||
|
@ -54,13 +55,16 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let thisAuthor = '';
|
||||
|
||||
let disposed = false;
|
||||
const outerWin = document.getElementsByName("ace_outer")[0]
|
||||
const targetDoc = outerWin.contentWindow.document.getElementsByName("ace_inner")[0].contentWindow.document
|
||||
const targetBody = targetDoc.body
|
||||
|
||||
const focus = () => {
|
||||
window.focus();
|
||||
targetBody.focus();
|
||||
};
|
||||
|
||||
const outerWin = window.parent;
|
||||
const outerDoc = outerWin.document;
|
||||
const outerDoc = outerWin.contentWindow.document;
|
||||
|
||||
const sideDiv = outerDoc.getElementById('sidediv');
|
||||
const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv');
|
||||
const sideDivInner = outerDoc.getElementById('sidedivinner');
|
||||
|
@ -74,7 +78,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
appendNewSideDivLine();
|
||||
|
||||
const scroll = Scroll.init(outerWin);
|
||||
const scroll = new Scroll(outerWin);
|
||||
|
||||
let outsideKeyDown = noop;
|
||||
let outsideKeyPress = (e) => true;
|
||||
|
@ -415,7 +419,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
const setWraps = (newVal) => {
|
||||
doesWrap = newVal;
|
||||
document.body.classList.toggle('doesWrap', doesWrap);
|
||||
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||
scheduler.setTimeout(() => {
|
||||
inCallStackIfNecessary('setWraps', () => {
|
||||
fastIncorp(7);
|
||||
|
@ -445,7 +449,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const setTextFace = (face) => {
|
||||
document.body.style.fontFamily = face;
|
||||
targetBody.style.fontFamily = face;
|
||||
lineMetricsDiv.style.fontFamily = face;
|
||||
};
|
||||
|
||||
|
@ -456,8 +460,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
const setEditable = (newVal) => {
|
||||
isEditable = newVal;
|
||||
document.body.contentEditable = isEditable ? 'true' : 'false';
|
||||
document.body.classList.toggle('static', !isEditable);
|
||||
targetBody.contentEditable = isEditable ? 'true' : 'false';
|
||||
targetBody.classList.toggle('static', !isEditable);
|
||||
};
|
||||
|
||||
const enforceEditability = () => setEditable(isEditable);
|
||||
|
@ -480,6 +484,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
newText = `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
|
||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||
setDocText(newText);
|
||||
});
|
||||
|
@ -640,8 +645,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// These properties are exposed
|
||||
const setters = {
|
||||
wraps: setWraps,
|
||||
showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val),
|
||||
showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val),
|
||||
showsauthorcolors: (val) => targetBody.classList.toggle('authorColors', !!val),
|
||||
showsuserselections: (val) => targetBody.classList.toggle('userSelections', !!val),
|
||||
showslinenumbers: (value) => {
|
||||
hasLineNumbers = !!value;
|
||||
sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers);
|
||||
|
@ -654,8 +659,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
styled: setStyled,
|
||||
textface: setTextFace,
|
||||
rtlistrue: (value) => {
|
||||
document.body.classList.toggle('rtl', value);
|
||||
document.body.classList.toggle('ltr', !value);
|
||||
targetBody.classList.toggle('rtl', value);
|
||||
targetBody.classList.toggle('ltr', !value);
|
||||
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||
},
|
||||
};
|
||||
|
@ -894,11 +899,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
clearObservedChanges();
|
||||
|
||||
const getCleanNodeByKey = (key) => {
|
||||
let n = document.getElementById(key);
|
||||
let n = targetDoc.getElementById(key);
|
||||
// copying and pasting can lead to duplicate ids
|
||||
while (n && isNodeDirty(n)) {
|
||||
n.id = '';
|
||||
n = document.getElementById(key);
|
||||
n = targetDoc.getElementById(key);
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
@ -980,11 +985,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const observeSuspiciousNodes = () => {
|
||||
// inspired by Firefox bug #473255, where pasting formatted text
|
||||
// causes the cursor to jump away, making the new HTML never found.
|
||||
if (document.body.getElementsByTagName) {
|
||||
const elts = document.body.getElementsByTagName('style');
|
||||
if (targetBody.getElementsByTagName) {
|
||||
const elts = targetBody.getElementsByTagName('style');
|
||||
for (const elt of elts) {
|
||||
const n = topLevel(elt);
|
||||
if (n && n.parentNode === document.body) {
|
||||
if (n && n.parentNode === targetBody) {
|
||||
observeChangesAroundNode(n);
|
||||
}
|
||||
}
|
||||
|
@ -999,8 +1004,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false;
|
||||
|
||||
// returns true if dom changes were made
|
||||
if (!document.body.firstChild) {
|
||||
document.body.innerHTML = '<div><!-- --></div>';
|
||||
if (!targetBody.firstChild) {
|
||||
targetBody.innerHTML = '<div><!-- --></div>';
|
||||
}
|
||||
|
||||
observeChangesAroundSelection();
|
||||
|
@ -1022,7 +1027,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
j++;
|
||||
}
|
||||
if (!dirtyRangesCheckOut) {
|
||||
for (const bodyNode of document.body.childNodes) {
|
||||
for (const bodyNode of targetBody.childNodes) {
|
||||
if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) {
|
||||
observeChangesAroundNode(bodyNode);
|
||||
}
|
||||
|
@ -1044,11 +1049,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const range = dirtyRanges[i];
|
||||
a = range[0];
|
||||
b = range[1];
|
||||
let firstDirtyNode = (((a === 0) && document.body.firstChild) ||
|
||||
let firstDirtyNode = (((a === 0) && targetBody.firstChild) ||
|
||||
getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling);
|
||||
firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode);
|
||||
|
||||
let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) ||
|
||||
let lastDirtyNode = (((b === rep.lines.length()) && targetBody.lastChild) ||
|
||||
getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling);
|
||||
|
||||
lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode);
|
||||
|
@ -1135,7 +1140,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
callstack: currentCallStack,
|
||||
editorInfo,
|
||||
rep,
|
||||
root: document.body,
|
||||
root: targetBody,
|
||||
point: selection.startPoint,
|
||||
documentAttributeManager,
|
||||
});
|
||||
|
@ -1147,7 +1152,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
callstack: currentCallStack,
|
||||
editorInfo,
|
||||
rep,
|
||||
root: document.body,
|
||||
root: targetBody,
|
||||
point: selection.endPoint,
|
||||
documentAttributeManager,
|
||||
});
|
||||
|
@ -1227,9 +1232,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
info.prepareForAdd();
|
||||
entry.lineMarker = info.lineMarker;
|
||||
if (!nodeToAddAfter) {
|
||||
document.body.insertBefore(node, document.body.firstChild);
|
||||
targetBody.insertBefore(node, targetBody.firstChild);
|
||||
} else {
|
||||
document.body.insertBefore(node, nodeToAddAfter.nextSibling);
|
||||
targetBody.insertBefore(node, nodeToAddAfter.nextSibling);
|
||||
}
|
||||
nodeToAddAfter = node;
|
||||
info.notifyAdded();
|
||||
|
@ -1326,7 +1331,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// Turn DOM node selection into [line,char] selection.
|
||||
// This method has to work when the DOM is not pristine,
|
||||
// assuming the point is not in a dirty node.
|
||||
if (point.node === document.body) {
|
||||
if (point.node === targetBody) {
|
||||
if (point.index === 0) {
|
||||
return [0, 0];
|
||||
} else {
|
||||
|
@ -1345,7 +1350,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
col = nodeText(n).length;
|
||||
}
|
||||
let parNode, prevSib;
|
||||
while ((parNode = n.parentNode) !== document.body) {
|
||||
while ((parNode = n.parentNode) !== targetBody) {
|
||||
if ((prevSib = n.previousSibling)) {
|
||||
n = prevSib;
|
||||
col += nodeText(n).length;
|
||||
|
@ -1398,7 +1403,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo));
|
||||
|
||||
for (const k of keysToDelete) {
|
||||
const n = document.getElementById(k);
|
||||
const n = targetDoc.getElementById(k);
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
|
||||
|
@ -2087,7 +2092,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const a = cleanNodeForIndex(i - 1);
|
||||
const b = cleanNodeForIndex(i);
|
||||
if ((!a) || (!b)) return false; // violates precondition
|
||||
if ((a === true) && (b === true)) return !document.body.firstChild;
|
||||
if ((a === true) && (b === true)) return !targetBody.firstChild;
|
||||
if ((a === true) && b.previousSibling) return false;
|
||||
if ((b === true) && a.nextSibling) return false;
|
||||
if ((a === true) || (b === true)) return true;
|
||||
|
@ -2232,7 +2237,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const isNodeDirty = (n) => {
|
||||
if (n.parentNode !== document.body) return true;
|
||||
if (n.parentNode !== targetBody) return true;
|
||||
const data = getAssoc(n, 'dirtiness');
|
||||
if (!data) return true;
|
||||
if (n.id !== data.nodeId) return true;
|
||||
|
@ -2856,7 +2861,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
updateBrowserSelectionFromRep();
|
||||
// get the current caret selection, can't use rep. here because that only gives
|
||||
// us the start position not the current
|
||||
const myselection = document.getSelection();
|
||||
const myselection = targetDoc.getSelection();
|
||||
// get the carets selection offset in px IE 214
|
||||
let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
|
||||
myselection.focusNode.offsetTop;
|
||||
|
@ -2970,13 +2975,13 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// with background doesn't seem to show up...
|
||||
if (isNodeText(p.node) && p.index === p.maxIndex) {
|
||||
let n = p.node;
|
||||
while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) {
|
||||
while (!n.nextSibling && n !== targetBody && n.parentNode !== targetBody) {
|
||||
n = n.parentNode;
|
||||
}
|
||||
if (n.nextSibling &&
|
||||
!(typeof n.nextSibling.tagName === 'string' &&
|
||||
n.nextSibling.tagName.toLowerCase() === 'br') &&
|
||||
n !== p.node && n !== document.body && n.parentNode !== document.body) {
|
||||
n !== p.node && n !== targetBody && n.parentNode !== targetBody) {
|
||||
// found a parent, go to next node and dive in
|
||||
p.node = n.nextSibling;
|
||||
p.maxIndex = nodeMaxIndex(p.node);
|
||||
|
@ -3003,7 +3008,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
}
|
||||
};
|
||||
const browserSelection = window.getSelection();
|
||||
const browserSelection = targetDoc.getSelection();
|
||||
if (browserSelection) {
|
||||
browserSelection.removeAllRanges();
|
||||
if (selection) {
|
||||
|
@ -3078,7 +3083,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// each of which has node (a magicdom node), index, and maxIndex. If the node
|
||||
// is a text node, maxIndex is the length of the text; else maxIndex is 1.
|
||||
// index is between 0 and maxIndex, inclusive.
|
||||
const browserSelection = window.getSelection();
|
||||
const browserSelection = targetDoc.getSelection();
|
||||
if (!browserSelection || browserSelection.type === 'None' ||
|
||||
browserSelection.rangeCount === 0) {
|
||||
return null;
|
||||
|
@ -3096,7 +3101,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!isInBody(container)) {
|
||||
// command-click in Firefox selects whole document, HEAD and BODY!
|
||||
return {
|
||||
node: document.body,
|
||||
node: targetBody,
|
||||
index: 0,
|
||||
maxIndex: 1,
|
||||
};
|
||||
|
@ -3146,7 +3151,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
browserSelection.anchorOffset === range.endOffset,
|
||||
};
|
||||
|
||||
if (selection.startPoint.node.ownerDocument !== window.document) {
|
||||
if (selection.startPoint.node.ownerDocument !== targetDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -3181,17 +3186,17 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
editorInfo.ace_getInInternationalComposition = () => inInternationalComposition;
|
||||
|
||||
const bindTheEventHandlers = () => {
|
||||
$(document).on('keydown', handleKeyEvent);
|
||||
$(document).on('keypress', handleKeyEvent);
|
||||
$(document).on('keyup', handleKeyEvent);
|
||||
$(document).on('click', handleClick);
|
||||
$(targetDoc).on('keydown', handleKeyEvent);
|
||||
$(targetDoc).on('keypress', handleKeyEvent);
|
||||
$(targetDoc).on('keyup', handleKeyEvent);
|
||||
$(targetDoc).on('click', handleClick);
|
||||
// dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer
|
||||
$(outerDoc).on('click', hideEditBarDropdowns);
|
||||
|
||||
// If non-nullish, pasting on a link should be suppressed.
|
||||
let suppressPasteOnLink = null;
|
||||
|
||||
$(document.body).on('auxclick', (e) => {
|
||||
$(targetBody).on('auxclick', (e) => {
|
||||
if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) {
|
||||
// The user middle-clicked on a link. Usually users do this to open a link in a new tab, but
|
||||
// in X11 (Linux) this will instead paste the contents of the primary selection at the mouse
|
||||
|
@ -3213,7 +3218,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
}
|
||||
});
|
||||
|
||||
$(document.body).on('paste', (e) => {
|
||||
$(targetBody).on('paste', (e) => {
|
||||
if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) {
|
||||
scheduler.clearTimeout(suppressPasteOnLink);
|
||||
suppressPasteOnLink = null;
|
||||
|
@ -3233,7 +3238,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// We reference document here, this is because if we don't this will expose a bug
|
||||
// in Google Chrome. This bug will cause the last character on the last line to
|
||||
// not fire an event when dropped into..
|
||||
$(document).on('drop', (e) => {
|
||||
$(targetBody).on('drop', (e) => {
|
||||
if (e.target.a || e.target.localName === 'a') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -3251,7 +3256,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const lineAfterSelection = lastLineSelected.nextSibling;
|
||||
|
||||
const neighbor = lineBeforeSelection || lineAfterSelection;
|
||||
neighbor.appendChild(document.createElement('style'));
|
||||
neighbor.appendChild(targetDoc.createElement('style'));
|
||||
}
|
||||
|
||||
// Call drop hook
|
||||
|
@ -3263,10 +3268,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
});
|
||||
});
|
||||
|
||||
$(document.documentElement).on('compositionstart', () => {
|
||||
$(targetDoc.documentElement).on('compositionstart', () => {
|
||||
if (inInternationalComposition) return;
|
||||
inInternationalComposition = new Promise((resolve) => {
|
||||
$(document.documentElement).one('compositionend', () => {
|
||||
$(targetDoc.documentElement).one('compositionend', () => {
|
||||
inInternationalComposition = null;
|
||||
resolve();
|
||||
});
|
||||
|
@ -3275,8 +3280,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const topLevel = (n) => {
|
||||
if ((!n) || n === document.body) return null;
|
||||
while (n.parentNode !== document.body) {
|
||||
if ((!n) || n === targetBody) return null;
|
||||
while (n.parentNode !== targetBody) {
|
||||
n = n.parentNode;
|
||||
}
|
||||
return n;
|
||||
|
@ -3436,10 +3441,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// but as it's non-text type the line-height/margins might not be present and it
|
||||
// could be that this breaks a theme that has a different default line height..
|
||||
// So instead of using an integer here we get the value from the Editor CSS.
|
||||
const innerdocbodyStyles = getComputedStyle(document.body);
|
||||
const innerdocbodyStyles = getComputedStyle(targetBody);
|
||||
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
|
||||
|
||||
for (const docLine of document.body.children) {
|
||||
for (const docLine of targetBody.children) {
|
||||
let h;
|
||||
const nextDocLine = docLine.nextElementSibling;
|
||||
if (nextDocLine) {
|
||||
|
@ -3450,7 +3455,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// included on the first line. The default stylesheet doesn't add
|
||||
// extra margins/padding, but plugins might.
|
||||
h = nextDocLine.offsetTop - parseInt(
|
||||
window.getComputedStyle(document.body)
|
||||
window.getComputedStyle(targetBody)
|
||||
.getPropertyValue('padding-top').split('px')[0]);
|
||||
} else {
|
||||
h = nextDocLine.offsetTop - docLine.offsetTop;
|
||||
|
@ -3496,15 +3501,15 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
this.init = async () => {
|
||||
await $.ready;
|
||||
inCallStack('setup', () => {
|
||||
if (browser.firefox) $(document.body).addClass('mozilla');
|
||||
if (browser.safari) $(document.body).addClass('safari');
|
||||
document.body.classList.toggle('authorColors', true);
|
||||
document.body.classList.toggle('doesWrap', doesWrap);
|
||||
if (browser.firefox) $(targetBody).addClass('mozilla');
|
||||
if (browser.safari) $(targetBody).addClass('safari');
|
||||
targetBody.classList.toggle('authorColors', true);
|
||||
targetBody.classList.toggle('doesWrap', doesWrap);
|
||||
|
||||
enforceEditability();
|
||||
|
||||
// set up dom and rep
|
||||
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
|
||||
while (targetBody.firstChild) targetBody.removeChild(targetBody.firstChild);
|
||||
const oneEntry = createDomLineEntry('');
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo]);
|
||||
|
|
|
@ -32,6 +32,9 @@ const colorutils = require('./colorutils').colorutils;
|
|||
const _ = require('./underscore');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
|
||||
// These parameters were global, now they are injected. A reference to the
|
||||
// Timeslider controller would probably be more appropriate.
|
||||
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
const _ = require('./underscore');
|
||||
const padmodals = require('./pad_modals').padmodals;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
||||
let BroadcastSlider;
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
// One rep.line(div) can be broken in more than one line in the browser.
|
||||
// This function is useful to get the caret position of the line as
|
||||
// is represented by the browser
|
||||
exports.getPosition = () => {
|
||||
import {Position, RepModel, RepNode} from "./types/RepModel";
|
||||
|
||||
export const getPosition = () => {
|
||||
const range = getSelectionRange();
|
||||
// @ts-ignore
|
||||
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
|
||||
// When there's a <br> or any element that has no height, we can't get the dimension of the
|
||||
// element where the caret is. As we can't get the element height, we create a text node to get
|
||||
|
@ -18,7 +21,7 @@ exports.getPosition = () => {
|
|||
return line;
|
||||
};
|
||||
|
||||
const createSelectionRange = (range) => {
|
||||
const createSelectionRange = (range: Range) => {
|
||||
const clonedRange = range.cloneRange();
|
||||
|
||||
// we set the selection start and end to avoid error when user selects a text bigger than
|
||||
|
@ -30,14 +33,14 @@ const createSelectionRange = (range) => {
|
|||
return clonedRange;
|
||||
};
|
||||
|
||||
const getPositionOfRepLineAtOffset = (node, offset) => {
|
||||
const getPositionOfRepLineAtOffset = (node: any, offset: number) => {
|
||||
// it is not a text node, so we cannot make a selection
|
||||
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
|
||||
return getPositionOfElementOrSelection(node);
|
||||
}
|
||||
|
||||
while (node.length === 0 && node.nextSibling) {
|
||||
node = node.nextSibling;
|
||||
node = node.nextSibling as any;
|
||||
}
|
||||
|
||||
const newRange = new Range();
|
||||
|
@ -48,14 +51,13 @@ const getPositionOfRepLineAtOffset = (node, offset) => {
|
|||
return linePosition;
|
||||
};
|
||||
|
||||
const getPositionOfElementOrSelection = (element) => {
|
||||
const getPositionOfElementOrSelection = (element: Range):Position => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const linePosition = {
|
||||
return {
|
||||
bottom: rect.bottom,
|
||||
height: rect.height,
|
||||
top: rect.top,
|
||||
};
|
||||
return linePosition;
|
||||
} satisfies Position;
|
||||
};
|
||||
|
||||
// here we have two possibilities:
|
||||
|
@ -64,7 +66,7 @@ const getPositionOfElementOrSelection = (element) => {
|
|||
// where is the top of the previous line
|
||||
// [2] the line before is part of another rep line. It's possible this line has different margins
|
||||
// height. So we have to get the exactly position of the line
|
||||
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
||||
export const getPositionTopOfPreviousBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
|
||||
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
|
||||
|
||||
|
@ -80,7 +82,7 @@ exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
|
|||
return previousLineTop;
|
||||
};
|
||||
|
||||
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
||||
const caretLineIsFirstBrowserLine = (caretLineTop: number, rep: RepModel) => {
|
||||
const caretRepLine = rep.selStart[0];
|
||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||
const firstRootNode = getFirstRootChildNode(lineNode);
|
||||
|
@ -91,7 +93,7 @@ const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
|
|||
};
|
||||
|
||||
// find the first root node, usually it is a text node
|
||||
const getFirstRootChildNode = (node) => {
|
||||
const getFirstRootChildNode = (node: RepNode) => {
|
||||
if (!node.firstChild) {
|
||||
return node;
|
||||
} else {
|
||||
|
@ -99,7 +101,7 @@ const getFirstRootChildNode = (node) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
|
||||
const getDimensionOfLastBrowserLineOfRepLine = (line: number, rep: RepModel) => {
|
||||
const lineNode = rep.lines.atIndex(line).lineNode;
|
||||
const lastRootChildNode = getLastRootChildNode(lineNode);
|
||||
|
||||
|
@ -109,7 +111,7 @@ const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
|
|||
return lastRootChildNodePosition;
|
||||
};
|
||||
|
||||
const getLastRootChildNode = (node) => {
|
||||
const getLastRootChildNode = (node: RepNode) => {
|
||||
if (!node.lastChild) {
|
||||
return {
|
||||
node,
|
||||
|
@ -125,7 +127,7 @@ const getLastRootChildNode = (node) => {
|
|||
// So, we can use the caret line to calculate the bottom of the line.
|
||||
// [2] the next line is part of another rep line.
|
||||
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
|
||||
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
||||
export const getBottomOfNextBrowserLine = (caretLinePosition: Position, rep: RepModel) => {
|
||||
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
|
||||
const isCaretLineLastBrowserLine =
|
||||
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
|
||||
|
@ -142,7 +144,7 @@ exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
|
|||
return nextLineBottom;
|
||||
};
|
||||
|
||||
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
||||
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop: number, rep: RepModel) => {
|
||||
const caretRepLine = rep.selStart[0];
|
||||
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
|
||||
const lastRootChildNode = getLastRootChildNode(lineNode);
|
||||
|
@ -153,7 +155,7 @@ const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
|
|||
return lastRootChildNodePosition.top === caretLineTop;
|
||||
};
|
||||
|
||||
const getPreviousVisibleLine = (line, rep) => {
|
||||
export const getPreviousVisibleLine = (line: number, rep: RepModel): number => {
|
||||
const firstLineOfPad = 0;
|
||||
if (line <= firstLineOfPad) {
|
||||
return firstLineOfPad;
|
||||
|
@ -165,9 +167,8 @@ const getPreviousVisibleLine = (line, rep) => {
|
|||
};
|
||||
|
||||
|
||||
exports.getPreviousVisibleLine = getPreviousVisibleLine;
|
||||
|
||||
const getNextVisibleLine = (line, rep) => {
|
||||
export const getNextVisibleLine = (line: number, rep: RepModel): number => {
|
||||
const lastLineOfThePad = rep.lines.length() - 1;
|
||||
if (line >= lastLineOfThePad) {
|
||||
return lastLineOfThePad;
|
||||
|
@ -177,11 +178,10 @@ const getNextVisibleLine = (line, rep) => {
|
|||
return getNextVisibleLine(line + 1, rep);
|
||||
}
|
||||
};
|
||||
exports.getNextVisibleLine = getNextVisibleLine;
|
||||
|
||||
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
||||
const isLineVisible = (line: number, rep: RepModel) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
|
||||
|
||||
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
|
||||
const getDimensionOfFirstBrowserLineOfRepLine = (line: number, rep: RepModel) => {
|
||||
const lineNode = rep.lines.atIndex(line).lineNode;
|
||||
const firstRootChildNode = getFirstRootChildNode(lineNode);
|
||||
|
|
@ -21,10 +21,12 @@ const padcookie = require('./pad_cookie').padcookie;
|
|||
const Tinycon = require('tinycon/tinycon');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
||||
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
|
||||
|
||||
exports.chat = (() => {
|
||||
let isStuck = false;
|
||||
let userAndChat = false;
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
((document) => {
|
||||
// Set language for l10n
|
||||
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
|
||||
if (language) language = language[1];
|
||||
|
||||
html10n.bind('indexed', () => {
|
||||
html10n.localize([language, navigator.language, navigator.userLanguage, 'en']);
|
||||
});
|
||||
|
||||
html10n.bind('localized', () => {
|
||||
document.documentElement.lang = html10n.getLanguage();
|
||||
document.documentElement.dir = html10n.getDirection();
|
||||
});
|
||||
})(document);
|
18
src/static/js/l10n.ts
Normal file
18
src/static/js/l10n.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import html10n from '../js/vendors/html10n';
|
||||
|
||||
|
||||
// Set language for l10n
|
||||
let regexpLang: string | undefined;
|
||||
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
|
||||
if (language) regexpLang = language[1];
|
||||
|
||||
html10n.mt.bind('indexed', () => {
|
||||
console.log('Navigator language', navigator.language)
|
||||
console.log('Localizing things', [regexpLang, navigator.language, 'en'])
|
||||
html10n.localize([regexpLang, navigator.language, 'en']);
|
||||
});
|
||||
|
||||
html10n.mt.bind('localized', () => {
|
||||
document.documentElement.lang = html10n.getLanguage()!;
|
||||
document.documentElement.dir = html10n.getDirection()!;
|
||||
});
|
|
@ -24,12 +24,15 @@
|
|||
|
||||
let socket;
|
||||
|
||||
|
||||
// These jQuery things should create local references, but for now `require()`
|
||||
// assigns to the global `$` and augments it with plugins.
|
||||
require('./vendors/jquery');
|
||||
require('./vendors/farbtastic');
|
||||
require('./vendors/gritter');
|
||||
|
||||
import html10n from './vendors/html10n'
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
const chat = require('./chat').chat;
|
||||
const getCollabClient = require('./collab_client').getCollabClient;
|
||||
|
@ -136,7 +139,8 @@ const getParameters = [
|
|||
name: 'lang',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
window.html10n.localize([val, 'en']);
|
||||
console.log('Val is', val)
|
||||
html10n.localize([val, 'en']);
|
||||
Cookies.set('language', val);
|
||||
},
|
||||
},
|
||||
|
@ -281,6 +285,7 @@ const handshake = async () => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('error', (error) => {
|
||||
// pad.collabClient might be null if the error occurred before the hanshake completed.
|
||||
if (pad.collabClient != null) {
|
||||
|
@ -313,6 +318,15 @@ const handshake = async () => {
|
|||
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
||||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||
}
|
||||
if(window.clientVars.mode === "development") {
|
||||
console.warn('Enabling development mode with live update')
|
||||
socket.on('liveupdate', ()=>{
|
||||
|
||||
console.log('Live reload update received')
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
} else if (obj.disconnect) {
|
||||
padconnectionstatus.disconnected(obj.disconnect);
|
||||
socket.disconnect();
|
||||
|
@ -713,7 +727,7 @@ const pad = {
|
|||
$.ajax(
|
||||
{
|
||||
type: 'post',
|
||||
url: 'ep/pad/connection-diagnostic-info',
|
||||
url: '../ep/pad/connection-diagnostic-info',
|
||||
data: {
|
||||
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
'use strict';
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
|
||||
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
||||
|
|
|
@ -24,9 +24,10 @@
|
|||
const Cookies = require('./pad_utils').Cookies;
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const Ace2Editor = require('./ace').Ace2Editor;
|
||||
import html10n from '../js/vendors/html10n'
|
||||
|
||||
const padeditor = (() => {
|
||||
let Ace2Editor = undefined;
|
||||
let pad = undefined;
|
||||
let settings = undefined;
|
||||
|
||||
|
@ -35,7 +36,6 @@ const padeditor = (() => {
|
|||
// this is accessed directly from other files
|
||||
viewZoom: 100,
|
||||
init: async (initialViewOptions, _pad) => {
|
||||
Ace2Editor = require('./ace').Ace2Editor;
|
||||
pad = _pad;
|
||||
settings = pad.settings;
|
||||
self.ace = new Ace2Editor();
|
||||
|
@ -99,7 +99,7 @@ const padeditor = (() => {
|
|||
$('#languagemenu').val(html10n.getLanguage());
|
||||
$('#languagemenu').on('change', () => {
|
||||
Cookies.set('language', $('#languagemenu').val());
|
||||
window.html10n.localize([$('#languagemenu').val(), 'en']);
|
||||
html10n.localize([$('#languagemenu').val(), 'en']);
|
||||
if ($('select').niceSelect) {
|
||||
$('select').niceSelect('update');
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
|
||||
|
||||
const padimpexp = (() => {
|
||||
let pad;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ let pad;
|
|||
|
||||
exports.saveNow = () => {
|
||||
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
|
||||
$.gritter.add({
|
||||
window.$.gritter.add({
|
||||
// (string | mandatory) the heading of the notification
|
||||
title: html10n.get('pad.savedrevs.marked'),
|
||||
// (string | mandatory) the text inside the notification
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
let myUserInfo = {};
|
||||
|
||||
let colorPickerOpen = false;
|
||||
|
|
|
@ -356,7 +356,6 @@ const padutils = {
|
|||
let globalExceptionHandler = null;
|
||||
padutils.setupGlobalExceptionHandler = () => {
|
||||
if (globalExceptionHandler == null) {
|
||||
require('./vendors/gritter');
|
||||
globalExceptionHandler = (e) => {
|
||||
let type;
|
||||
let err;
|
||||
|
@ -443,7 +442,7 @@ const inThirdPartyIframe = () => {
|
|||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||
// window object.
|
||||
if (typeof window !== 'undefined') {
|
||||
exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
|
||||
exports.Cookies = require('js-cookie').withAttributes({
|
||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||
|
|
|
@ -7,24 +7,13 @@ exports.baseURL = '';
|
|||
|
||||
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
|
||||
|
||||
exports.update = (cb) => {
|
||||
// It appears that this response (see #620) may interrupt the current thread
|
||||
// of execution on Firefox. This schedules the response in the run-loop,
|
||||
// which appears to fix the issue.
|
||||
const callback = () => setTimeout(cb, 0);
|
||||
|
||||
jQuery.getJSON(
|
||||
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
|
||||
).done((data) => {
|
||||
defs.plugins = data.plugins;
|
||||
defs.parts = data.parts;
|
||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
|
||||
defs.loaded = true;
|
||||
callback();
|
||||
}).fail((err) => {
|
||||
console.error(`Failed to load plugin-definitions: ${err}`);
|
||||
callback();
|
||||
});
|
||||
exports.update = async (modules) => {
|
||||
const data = await jQuery.getJSON(
|
||||
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
|
||||
defs.plugins = data.plugins;
|
||||
defs.parts = data.parts;
|
||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
|
||||
defs.loaded = true;
|
||||
};
|
||||
|
||||
const adoptPluginsFromAncestorsOf = (frame) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ const disabledHookReasons = {
|
|||
},
|
||||
};
|
||||
|
||||
const loadFn = (path, hookName) => {
|
||||
const loadFn = (path, hookName, modules) => {
|
||||
let functionName;
|
||||
const parts = path.split(':');
|
||||
|
||||
|
@ -24,7 +24,13 @@ const loadFn = (path, hookName) => {
|
|||
functionName = parts[1];
|
||||
}
|
||||
|
||||
let fn = require(path);
|
||||
let fn
|
||||
if (modules === undefined || !("get" in modules)) {
|
||||
fn = require(/* webpackIgnore: true */ path);
|
||||
} else {
|
||||
fn = modules.get(path);
|
||||
}
|
||||
|
||||
functionName = functionName ? functionName : hookName;
|
||||
|
||||
for (const name of functionName.split('.')) {
|
||||
|
@ -33,7 +39,7 @@ const loadFn = (path, hookName) => {
|
|||
return fn;
|
||||
};
|
||||
|
||||
const extractHooks = (parts, hookSetName, normalizer) => {
|
||||
const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
||||
const hooks = {};
|
||||
for (const part of parts) {
|
||||
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||
|
@ -53,7 +59,7 @@ const extractHooks = (parts, hookSetName, normalizer) => {
|
|||
}
|
||||
let hookFn;
|
||||
try {
|
||||
hookFn = loadFn(hookFnName, hookName);
|
||||
hookFn = loadFn(hookFnName, hookName, modules);
|
||||
if (!hookFn) throw new Error('Not a function');
|
||||
} catch (err) {
|
||||
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
This file handles scroll on edition or when user presses arrow keys.
|
||||
In this file we have two representations of line (browser and rep line).
|
||||
Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
|
||||
Browser Line = each vertical line. A <div> can be break into more than one
|
||||
browser line.
|
||||
*/
|
||||
const caretPosition = require('./caretPosition');
|
||||
|
||||
function Scroll(outerWin) {
|
||||
// scroll settings
|
||||
this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||
|
||||
// DOM reference
|
||||
this.outerWin = outerWin;
|
||||
this.doc = this.outerWin.document;
|
||||
this.rootDocument = parent.parent.document;
|
||||
}
|
||||
|
||||
Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
|
||||
function (rep, isScrollableEvent, innerHeight) {
|
||||
// are we placing the caret on the line at the bottom of viewport?
|
||||
// And if so, do we need to scroll the editor, as defined on the settings.json?
|
||||
const shouldScrollWhenCaretIsAtBottomOfViewport =
|
||||
this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
|
||||
if (shouldScrollWhenCaretIsAtBottomOfViewport) {
|
||||
// avoid scrolling when selection includes multiple lines --
|
||||
// user can potentially be selecting more lines
|
||||
// than it fits on viewport
|
||||
const multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
|
||||
|
||||
// avoid scrolling when pad loads
|
||||
if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
|
||||
// when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
|
||||
const pixelsToScroll = this._getPixelsRelativeToPercentageOfViewport(innerHeight);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scroll.prototype.scrollWhenPressArrowKeys = function (arrowUp, rep, innerHeight) {
|
||||
// if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
|
||||
// rep line on the top of the viewport
|
||||
if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) {
|
||||
const pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
|
||||
|
||||
// by default, the browser scrolls to the middle of the viewport. To avoid the twist made
|
||||
// when we apply a second scroll, we made it immediately (without animation)
|
||||
this._scrollYPageWithoutAnimation(-pixelsToScroll);
|
||||
} else {
|
||||
this.scrollNodeVerticallyIntoView(rep, innerHeight);
|
||||
}
|
||||
};
|
||||
|
||||
// Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking
|
||||
// if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are
|
||||
// other lines after caretLine(), and all of them are out of viewport.
|
||||
Scroll.prototype._isCaretAtTheBottomOfViewport = function (rep) {
|
||||
// computing a line position using getBoundingClientRect() is expensive.
|
||||
// (obs: getBoundingClientRect() is called on caretPosition.getPosition())
|
||||
// To avoid that, we only call this function when it is possible that the
|
||||
// caret is in the bottom of viewport
|
||||
const caretLine = rep.selStart[0];
|
||||
const lineAfterCaretLine = caretLine + 1;
|
||||
const firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
|
||||
const caretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||
const lineAfterCaretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
|
||||
if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
|
||||
// check if the caret is in the bottom of the viewport
|
||||
const caretLinePosition = caretPosition.getPosition();
|
||||
const viewportBottom = this._getViewPortTopBottom().bottom;
|
||||
const nextLineBottom = caretPosition.getBottomOfNextBrowserLine(caretLinePosition, rep);
|
||||
const nextLineIsBelowViewportBottom = nextLineBottom > viewportBottom;
|
||||
return nextLineIsBelowViewportBottom;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep) {
|
||||
const lineNode = rep.lines.atIndex(lineNumber);
|
||||
const linePosition = this._getLineEntryTopBottom(lineNode);
|
||||
const lineTop = linePosition.top;
|
||||
const lineBottom = linePosition.bottom;
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
const viewportBottom = viewport.bottom;
|
||||
const viewportTop = viewport.top;
|
||||
|
||||
const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
|
||||
const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
|
||||
const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
|
||||
const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
|
||||
const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
|
||||
const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
|
||||
|
||||
return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
|
||||
(topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
|
||||
(bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
|
||||
};
|
||||
|
||||
Scroll.prototype._getViewPortTopBottom = function () {
|
||||
const theTop = this.getScrollY();
|
||||
const doc = this.doc;
|
||||
const height = doc.documentElement.clientHeight; // includes padding
|
||||
|
||||
// we have to get the exactly height of the viewport.
|
||||
// So it has to subtract all the values which changes
|
||||
// the viewport height (E.g. padding, position top)
|
||||
const viewportExtraSpacesAndPosition =
|
||||
this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
|
||||
return {
|
||||
top: theTop,
|
||||
bottom: (theTop + height - viewportExtraSpacesAndPosition),
|
||||
};
|
||||
};
|
||||
|
||||
Scroll.prototype._getEditorPositionTop = function () {
|
||||
const editor = parent.document.getElementsByTagName('iframe');
|
||||
const editorPositionTop = editor[0].offsetTop;
|
||||
return editorPositionTop;
|
||||
};
|
||||
|
||||
// ep_page_view adds padding-top, which makes the viewport smaller
|
||||
Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function () {
|
||||
const aceOuter = this.rootDocument.getElementsByName('ace_outer');
|
||||
const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
|
||||
return aceOuterPaddingTop;
|
||||
};
|
||||
|
||||
Scroll.prototype._getScrollXY = function () {
|
||||
const win = this.outerWin;
|
||||
const odoc = this.doc;
|
||||
if (typeof (win.pageYOffset) === 'number') {
|
||||
return {
|
||||
x: win.pageXOffset,
|
||||
y: win.pageYOffset,
|
||||
};
|
||||
}
|
||||
const docel = odoc.documentElement;
|
||||
if (docel && typeof (docel.scrollTop) === 'number') {
|
||||
return {
|
||||
x: docel.scrollLeft,
|
||||
y: docel.scrollTop,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Scroll.prototype.getScrollX = function () {
|
||||
return this._getScrollXY().x;
|
||||
};
|
||||
|
||||
Scroll.prototype.getScrollY = function () {
|
||||
return this._getScrollXY().y;
|
||||
};
|
||||
|
||||
Scroll.prototype.setScrollX = function (x) {
|
||||
this.outerWin.scrollTo(x, this.getScrollY());
|
||||
};
|
||||
|
||||
Scroll.prototype.setScrollY = function (y) {
|
||||
this.outerWin.scrollTo(this.getScrollX(), y);
|
||||
};
|
||||
|
||||
Scroll.prototype.setScrollXY = function (x, y) {
|
||||
this.outerWin.scrollTo(x, y);
|
||||
};
|
||||
|
||||
Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
|
||||
const caretLine = rep.selStart[0];
|
||||
const linePrevCaretLine = caretLine - 1;
|
||||
const firstLineVisibleBeforeCaretLine =
|
||||
caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
|
||||
const caretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||
const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
|
||||
if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
|
||||
const caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
|
||||
const viewportPosition = this._getViewPortTopBottom();
|
||||
const viewportTop = viewportPosition.top;
|
||||
const viewportBottom = viewportPosition.bottom;
|
||||
const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
|
||||
const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
|
||||
const caretLineIsInsideOfViewport =
|
||||
caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
|
||||
if (caretLineIsInsideOfViewport) {
|
||||
const prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
|
||||
const previousLineIsAboveViewportTop = prevLineTop < viewportTop;
|
||||
return previousLineIsAboveViewportTop;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// By default, when user makes an edition in a line out of viewport, this line goes
|
||||
// to the edge of viewport. This function gets the extra pixels necessary to get the
|
||||
// caret line in a position X relative to Y% viewport.
|
||||
Scroll.prototype._getPixelsRelativeToPercentageOfViewport =
|
||||
function (innerHeight, aboveOfViewport) {
|
||||
let pixels = 0;
|
||||
const scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport);
|
||||
if (scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1) {
|
||||
pixels = parseInt(innerHeight * scrollPercentageRelativeToViewport);
|
||||
}
|
||||
return pixels;
|
||||
};
|
||||
|
||||
// we use different percentages when change selection. It depends on if it is
|
||||
// either above the top or below the bottom of the page
|
||||
Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) {
|
||||
let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
|
||||
if (aboveOfViewport) {
|
||||
percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
|
||||
}
|
||||
return percentageToScroll;
|
||||
};
|
||||
|
||||
Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeight) {
|
||||
let pixels = 0;
|
||||
const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||
if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) {
|
||||
pixels = parseInt(innerHeight * percentageToScrollUp);
|
||||
}
|
||||
return pixels;
|
||||
};
|
||||
|
||||
Scroll.prototype._scrollYPage = function (pixelsToScroll) {
|
||||
const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
|
||||
if (durationOfAnimationToShowFocusline) {
|
||||
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
} else {
|
||||
this._scrollYPageWithoutAnimation(pixelsToScroll);
|
||||
}
|
||||
};
|
||||
|
||||
Scroll.prototype._scrollYPageWithoutAnimation = function (pixelsToScroll) {
|
||||
this.outerWin.scrollBy(0, pixelsToScroll);
|
||||
};
|
||||
|
||||
Scroll.prototype._scrollYPageWithAnimation =
|
||||
function (pixelsToScroll, durationOfAnimationToShowFocusline) {
|
||||
const outerDocBody = this.doc.getElementById('outerdocbody');
|
||||
|
||||
// it works on later versions of Chrome
|
||||
const $outerDocBody = $(outerDocBody);
|
||||
this._triggerScrollWithAnimation(
|
||||
$outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
|
||||
// it works on Firefox and earlier versions of Chrome
|
||||
const $outerDocBodyParent = $outerDocBody.parent();
|
||||
this._triggerScrollWithAnimation(
|
||||
$outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
};
|
||||
|
||||
// using a custom queue and clearing it, we avoid creating a queue of scroll animations.
|
||||
// So if this function is called twice quickly, only the last one runs.
|
||||
Scroll.prototype._triggerScrollWithAnimation =
|
||||
function ($elem, pixelsToScroll, durationOfAnimationToShowFocusline) {
|
||||
// clear the queue of animation
|
||||
$elem.stop('scrollanimation');
|
||||
$elem.animate({
|
||||
scrollTop: `+=${pixelsToScroll}`,
|
||||
}, {
|
||||
duration: durationOfAnimationToShowFocusline,
|
||||
queue: 'scrollanimation',
|
||||
}).dequeue('scrollanimation');
|
||||
};
|
||||
|
||||
// scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance
|
||||
// needed to be completely in view. If the value is greater than 0 and less than or equal to 1,
|
||||
// besides of scrolling the minimum needed to be visible, it scrolls additionally
|
||||
// (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels
|
||||
Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
|
||||
// when the selection changes outside of the viewport the browser automatically scrolls the line
|
||||
// to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
|
||||
// So, when the line scrolled gets outside of the viewport we let the browser handle it.
|
||||
const linePosition = caretPosition.getPosition();
|
||||
if (linePosition) {
|
||||
const distanceOfTopOfViewport = linePosition.top - viewport.top;
|
||||
const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom - linePosition.height;
|
||||
const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
|
||||
const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
|
||||
if (caretIsAboveOfViewport) {
|
||||
const pixelsToScroll =
|
||||
distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
} else if (caretIsBelowOfViewport) {
|
||||
// setTimeout is required here as line might not be fully rendered onto the pad
|
||||
setTimeout(() => {
|
||||
const outer = window.parent;
|
||||
// scroll to the very end of the pad outer
|
||||
outer.scrollTo(0, outer[0].innerHeight);
|
||||
}, 150);
|
||||
// if the above setTimeout and functionality is removed then hitting an enter
|
||||
// key while on the last line wont be an optimal user experience
|
||||
// Details at: https://github.com/ether/etherpad-lite/pull/4639/files
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scroll.prototype._partOfRepLineIsOutOfViewport = function (viewportPosition, rep) {
|
||||
const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
|
||||
const line = rep.lines.atIndex(focusLine);
|
||||
const linePosition = this._getLineEntryTopBottom(line);
|
||||
const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
|
||||
const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
|
||||
|
||||
return lineIsBelowOfViewport || lineIsAboveOfViewport;
|
||||
};
|
||||
|
||||
Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
|
||||
const dom = entry.lineNode;
|
||||
const top = dom.offsetTop;
|
||||
const height = dom.offsetHeight;
|
||||
const obj = (destObj || {});
|
||||
obj.top = top;
|
||||
obj.bottom = (top + height);
|
||||
return obj;
|
||||
};
|
||||
|
||||
Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function (arrowUp, rep) {
|
||||
const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
|
||||
};
|
||||
|
||||
Scroll.prototype.getVisibleLineRange = function (rep) {
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
// console.log("viewport top/bottom: %o", viewport);
|
||||
const obj = {};
|
||||
const self = this;
|
||||
const start = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).bottom > viewport.top);
|
||||
// return the first line that the top position is greater or equal than
|
||||
// the viewport. That is the first line that is below the viewport bottom.
|
||||
// So the line that is in the bottom of the viewport is the very previous one.
|
||||
let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom);
|
||||
if (end < start) end = start; // unlikely
|
||||
// top.console.log(start+","+(end -1));
|
||||
return [start, end - 1];
|
||||
};
|
||||
|
||||
Scroll.prototype.getVisibleCharRange = function (rep) {
|
||||
const lineRange = this.getVisibleLineRange(rep);
|
||||
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
|
||||
};
|
||||
|
||||
exports.init = (outerWin) => new Scroll(outerWin);
|
338
src/static/js/scroll.ts
Normal file
338
src/static/js/scroll.ts
Normal file
|
@ -0,0 +1,338 @@
|
|||
import {getBottomOfNextBrowserLine, getNextVisibleLine, getPosition, getPositionTopOfPreviousBrowserLine, getPreviousVisibleLine} from './caretPosition';
|
||||
import {Position, RepModel, RepNode, WindowElementWithScrolling} from "./types/RepModel";
|
||||
|
||||
|
||||
class Scroll {
|
||||
private readonly outerWin: HTMLIFrameElement;
|
||||
private readonly doc: Document;
|
||||
private rootDocument: Document;
|
||||
private scrollSettings: any;
|
||||
|
||||
constructor(outerWin: HTMLIFrameElement) {
|
||||
// @ts-ignore
|
||||
this.scrollSettings = window.clientVars.scrollWhenFocusLineIsOutOfViewport;
|
||||
|
||||
// DOM reference
|
||||
this.outerWin = outerWin;
|
||||
this.doc = this.outerWin.contentDocument!;
|
||||
this.rootDocument = parent.parent.document;
|
||||
}
|
||||
|
||||
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {
|
||||
// are we placing the caret on the line at the bottom of viewport?
|
||||
// And if so, do we need to scroll the editor, as defined on the settings.json?
|
||||
const shouldScrollWhenCaretIsAtBottomOfViewport =
|
||||
this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
|
||||
if (shouldScrollWhenCaretIsAtBottomOfViewport) {
|
||||
// avoid scrolling when selection includes multiple lines --
|
||||
// user can potentially be selecting more lines
|
||||
// than it fits on viewport
|
||||
const multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
|
||||
|
||||
// avoid scrolling when pad loads
|
||||
if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
|
||||
// when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
|
||||
const pixelsToScroll = this._getPixelsRelativeToPercentageOfViewport(innerHeight);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollWhenPressArrowKeys(arrowUp: boolean, rep: RepModel, innerHeight: number) {
|
||||
// if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
|
||||
// rep line on the top of the viewport
|
||||
if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) {
|
||||
const pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
|
||||
|
||||
// by default, the browser scrolls to the middle of the viewport. To avoid the twist made
|
||||
// when we apply a second scroll, we made it immediately (without animation)
|
||||
this._scrollYPageWithoutAnimation(-pixelsToScroll);
|
||||
} else {
|
||||
this.scrollNodeVerticallyIntoView(rep, innerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
_isCaretAtTheBottomOfViewport(rep: RepModel) {
|
||||
// computing a line position using getBoundingClientRect() is expensive.
|
||||
// (obs: getBoundingClientRect() is called on caretPosition.getPosition())
|
||||
// To avoid that, we only call this function when it is possible that the
|
||||
// caret is in the bottom of viewport
|
||||
const caretLine = rep.selStart[0];
|
||||
const lineAfterCaretLine = caretLine + 1;
|
||||
const firstLineVisibleAfterCaretLine = getNextVisibleLine(lineAfterCaretLine, rep);
|
||||
const caretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||
const lineAfterCaretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
|
||||
if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
|
||||
// check if the caret is in the bottom of the viewport
|
||||
const caretLinePosition = getPosition()!;
|
||||
const viewportBottom = this._getViewPortTopBottom().bottom;
|
||||
const nextLineBottom = getBottomOfNextBrowserLine(caretLinePosition, rep);
|
||||
return nextLineBottom > viewportBottom;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
_isLinePartiallyVisibleOnViewport(lineNumber: number, rep: RepModel){
|
||||
const lineNode = rep.lines.atIndex(lineNumber);
|
||||
const linePosition = this._getLineEntryTopBottom(lineNode);
|
||||
const lineTop = linePosition.top;
|
||||
const lineBottom = linePosition.bottom;
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
const viewportBottom = viewport.bottom;
|
||||
const viewportTop = viewport.top;
|
||||
|
||||
const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
|
||||
const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
|
||||
const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
|
||||
const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
|
||||
const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
|
||||
const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
|
||||
|
||||
return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
|
||||
(topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
|
||||
(bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
|
||||
};
|
||||
|
||||
_getViewPortTopBottom() {
|
||||
const theTop = this.getScrollY();
|
||||
const doc = this.doc;
|
||||
const height = doc.documentElement.clientHeight; // includes padding
|
||||
|
||||
// we have to get the exactly height of the viewport.
|
||||
// So it has to subtract all the values which changes
|
||||
// the viewport height (E.g. padding, position top)
|
||||
const viewportExtraSpacesAndPosition =
|
||||
this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
|
||||
return {
|
||||
top: theTop,
|
||||
bottom: (theTop + height - viewportExtraSpacesAndPosition),
|
||||
};
|
||||
};
|
||||
|
||||
_getEditorPositionTop() {
|
||||
const editor = parent.document.getElementsByTagName('iframe');
|
||||
const editorPositionTop = editor[0].offsetTop;
|
||||
return editorPositionTop;
|
||||
};
|
||||
|
||||
_getPaddingTopAddedWhenPageViewIsEnable() {
|
||||
const aceOuter = this.rootDocument.getElementsByName('ace_outer');
|
||||
const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
|
||||
return aceOuterPaddingTop;
|
||||
};
|
||||
|
||||
_getScrollXY() {
|
||||
const win = this.outerWin as WindowElementWithScrolling;
|
||||
const odoc = this.doc;
|
||||
if (typeof (win.pageYOffset) === 'number') {
|
||||
return {
|
||||
x: win.pageXOffset,
|
||||
y: win.pageYOffset,
|
||||
};
|
||||
}
|
||||
const docel = odoc.documentElement;
|
||||
if (docel && typeof (docel.scrollTop) === 'number') {
|
||||
return {
|
||||
x: docel.scrollLeft,
|
||||
y: docel.scrollTop,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getScrollX() {
|
||||
return this._getScrollXY()!.x;
|
||||
};
|
||||
|
||||
getScrollY () {
|
||||
return this._getScrollXY()!.y;
|
||||
};
|
||||
|
||||
setScrollX(x: number) {
|
||||
this.outerWin.scrollTo(x, this.getScrollY());
|
||||
};
|
||||
|
||||
setScrollY(y: number) {
|
||||
this.outerWin.scrollTo(this.getScrollX(), y);
|
||||
};
|
||||
|
||||
setScrollXY(x: number, y: number) {
|
||||
this.outerWin.scrollTo(x, y);
|
||||
};
|
||||
|
||||
_isCaretAtTheTopOfViewport(rep: RepModel) {
|
||||
const caretLine = rep.selStart[0];
|
||||
const linePrevCaretLine = caretLine - 1;
|
||||
const firstLineVisibleBeforeCaretLine =
|
||||
getPreviousVisibleLine(linePrevCaretLine, rep);
|
||||
const caretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
|
||||
const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
|
||||
this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
|
||||
if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
|
||||
const caretLinePosition = getPosition(); // get the position of the browser line
|
||||
const viewportPosition = this._getViewPortTopBottom();
|
||||
const viewportTop = viewportPosition.top;
|
||||
const viewportBottom = viewportPosition.bottom;
|
||||
const caretLineIsBelowViewportTop = caretLinePosition!.bottom >= viewportTop;
|
||||
const caretLineIsAboveViewportBottom = caretLinePosition!.top < viewportBottom;
|
||||
const caretLineIsInsideOfViewport =
|
||||
caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
|
||||
if (caretLineIsInsideOfViewport) {
|
||||
const prevLineTop = getPositionTopOfPreviousBrowserLine(caretLinePosition!, rep);
|
||||
const previousLineIsAboveViewportTop = prevLineTop < viewportTop;
|
||||
return previousLineIsAboveViewportTop;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// By default, when user makes an edition in a line out of viewport, this line goes
|
||||
// to the edge of viewport. This function gets the extra pixels necessary to get the
|
||||
// caret line in a position X relative to Y% viewport.
|
||||
_getPixelsRelativeToPercentageOfViewport(innerHeight: number, aboveOfViewport?: boolean) {
|
||||
let pixels = 0;
|
||||
const scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport);
|
||||
if (scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1) {
|
||||
pixels = parseInt(String(innerHeight * scrollPercentageRelativeToViewport));
|
||||
}
|
||||
return pixels;
|
||||
};
|
||||
|
||||
// we use different percentages when change selection. It depends on if it is
|
||||
// either above the top or below the bottom of the page
|
||||
_getPercentageToScroll(aboveOfViewport: boolean|undefined) {
|
||||
let percentageToScroll = this.scrollSettings.percentage.editionBelowViewport;
|
||||
if (aboveOfViewport) {
|
||||
percentageToScroll = this.scrollSettings.percentage.editionAboveViewport;
|
||||
}
|
||||
return percentageToScroll;
|
||||
};
|
||||
|
||||
_getPixelsToScrollWhenUserPressesArrowUp(innerHeight: number) {
|
||||
let pixels = 0;
|
||||
const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||
if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) {
|
||||
pixels = parseInt(String(innerHeight * percentageToScrollUp));
|
||||
}
|
||||
return pixels;
|
||||
};
|
||||
|
||||
_scrollYPage(pixelsToScroll: number) {
|
||||
const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
|
||||
if (durationOfAnimationToShowFocusline) {
|
||||
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
} else {
|
||||
this._scrollYPageWithoutAnimation(pixelsToScroll);
|
||||
}
|
||||
};
|
||||
|
||||
_scrollYPageWithoutAnimation(pixelsToScroll: number) {
|
||||
this.outerWin.scrollBy(0, pixelsToScroll);
|
||||
};
|
||||
|
||||
_scrollYPageWithAnimation(pixelsToScroll: number, durationOfAnimationToShowFocusline: number) {
|
||||
const outerDocBody = this.doc.getElementById('outerdocbody');
|
||||
|
||||
// it works on later versions of Chrome
|
||||
const $outerDocBody = $(outerDocBody!);
|
||||
this._triggerScrollWithAnimation(
|
||||
$outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
|
||||
// it works on Firefox and earlier versions of Chrome
|
||||
const $outerDocBodyParent = $outerDocBody.parent();
|
||||
this._triggerScrollWithAnimation(
|
||||
$outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
|
||||
};
|
||||
|
||||
_triggerScrollWithAnimation($elem:any, pixelsToScroll: number, durationOfAnimationToShowFocusline: number) {
|
||||
// clear the queue of animation
|
||||
$elem.stop('scrollanimation');
|
||||
$elem.animate({
|
||||
scrollTop: `+=${pixelsToScroll}`,
|
||||
}, {
|
||||
duration: durationOfAnimationToShowFocusline,
|
||||
queue: 'scrollanimation',
|
||||
}).dequeue('scrollanimation');
|
||||
};
|
||||
|
||||
|
||||
|
||||
scrollNodeVerticallyIntoView(rep: RepModel, innerHeight: number) {
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
|
||||
// when the selection changes outside of the viewport the browser automatically scrolls the line
|
||||
// to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
|
||||
// So, when the line scrolled gets outside of the viewport we let the browser handle it.
|
||||
const linePosition = getPosition();
|
||||
if (linePosition) {
|
||||
const distanceOfTopOfViewport = linePosition.top - viewport.top;
|
||||
const distanceOfBottomOfViewport = viewport.bottom - linePosition.bottom - linePosition.height;
|
||||
const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
|
||||
const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
|
||||
if (caretIsAboveOfViewport) {
|
||||
const pixelsToScroll =
|
||||
distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
|
||||
this._scrollYPage(pixelsToScroll);
|
||||
} else if (caretIsBelowOfViewport) {
|
||||
// setTimeout is required here as line might not be fully rendered onto the pad
|
||||
setTimeout(() => {
|
||||
const outer = window.parent;
|
||||
// scroll to the very end of the pad outer
|
||||
outer.scrollTo(0, outer[0].innerHeight);
|
||||
}, 150);
|
||||
// if the above setTimeout and functionality is removed then hitting an enter
|
||||
// key while on the last line wont be an optimal user experience
|
||||
// Details at: https://github.com/ether/etherpad-lite/pull/4639/files
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_partOfRepLineIsOutOfViewport(viewportPosition: Position, rep: RepModel) {
|
||||
const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
|
||||
const line = rep.lines.atIndex(focusLine);
|
||||
const linePosition = this._getLineEntryTopBottom(line);
|
||||
const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
|
||||
const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
|
||||
|
||||
return lineIsBelowOfViewport || lineIsAboveOfViewport;
|
||||
};
|
||||
|
||||
_getLineEntryTopBottom(entry: RepNode, destObj?: Position) {
|
||||
const dom = entry.lineNode;
|
||||
const top = dom.offsetTop;
|
||||
const height = dom.offsetHeight;
|
||||
const obj = (destObj || {}) as Position;
|
||||
obj.top = top;
|
||||
obj.bottom = (top + height);
|
||||
return obj;
|
||||
};
|
||||
|
||||
_arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp: boolean, rep: RepModel) {
|
||||
const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
|
||||
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
|
||||
};
|
||||
|
||||
getVisibleLineRange(rep: RepModel) {
|
||||
const viewport = this._getViewPortTopBottom();
|
||||
// console.log("viewport top/bottom: %o", viewport);
|
||||
const obj = {} as Position;
|
||||
const self = this;
|
||||
const start = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).bottom > viewport.top);
|
||||
// return the first line that the top position is greater or equal than
|
||||
// the viewport. That is the first line that is below the viewport bottom.
|
||||
// So the line that is in the bottom of the viewport is the very previous one.
|
||||
let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom);
|
||||
if (end < start) end = start; // unlikely
|
||||
// top.console.log(start+","+(end -1));
|
||||
return [start, end - 1];
|
||||
};
|
||||
|
||||
getVisibleCharRange(rep: RepModel) {
|
||||
const lineRange = this.getVisibleLineRange(rep);
|
||||
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
|
||||
};
|
||||
}
|
||||
|
||||
export default Scroll
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
/**
|
||||
* Creates a socket.io connection.
|
||||
|
|
|
@ -31,7 +31,7 @@ const randomString = require('./pad_utils').randomString;
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const socketio = require('./socketio');
|
||||
|
||||
import html10n from '../js/vendors/html10n'
|
||||
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
|
||||
|
||||
const init = () => {
|
||||
|
@ -117,6 +117,14 @@ const handleClientVars = (message) => {
|
|||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||
}
|
||||
|
||||
if(window.clientVars.mode === "development") {
|
||||
console.warn('Enabling development mode with live update')
|
||||
socket.on('liveupdate', ()=>{
|
||||
console.log('Doing live reload')
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
// load all script that doesn't work without the clientVars
|
||||
BroadcastSlider = require('./broadcast_slider')
|
||||
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
||||
|
|
31
src/static/js/types/RepModel.ts
Normal file
31
src/static/js/types/RepModel.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
export type RepModel = {
|
||||
lines: {
|
||||
atIndex: (num: number)=>RepNode,
|
||||
offsetOfIndex: (range: number)=>number,
|
||||
search: (filter: (e: RepNode)=>boolean)=>number,
|
||||
length: ()=>number
|
||||
}
|
||||
selStart: number[],
|
||||
selEnd: number[],
|
||||
selFocusAtStart: boolean
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
bottom: number,
|
||||
height: number,
|
||||
top: number
|
||||
}
|
||||
|
||||
export type RepNode = {
|
||||
firstChild: RepNode,
|
||||
lineNode: RepNode
|
||||
length: number,
|
||||
lastChild: RepNode,
|
||||
offsetHeight: number,
|
||||
offsetTop: number
|
||||
}
|
||||
|
||||
export type WindowElementWithScrolling = HTMLIFrameElement & {
|
||||
pageYOffset: number|string,
|
||||
pageXOffset: number
|
||||
}
|
7
src/static/js/vendors/farbtastic.js
vendored
7
src/static/js/vendors/farbtastic.js
vendored
|
@ -7,6 +7,7 @@
|
|||
// Licensed under the terms of the GNU General Public License v2.0:
|
||||
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
|
||||
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
|
||||
|
||||
(function ($) {
|
||||
|
||||
var __debug = false;
|
||||
|
@ -172,7 +173,7 @@ $._farbtastic = function (container, options) {
|
|||
angle2 = d2 * Math.PI * 2,
|
||||
// Endpoints
|
||||
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
|
||||
x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||
let x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
|
||||
// Midpoint chosen so that the endpoints are tangent to the circle.
|
||||
am = (angle1 + angle2) / 2,
|
||||
tan = 1 / Math.cos((angle2 - angle1) / 2),
|
||||
|
@ -329,8 +330,8 @@ $._farbtastic = function (container, options) {
|
|||
|
||||
// Update the overlay canvas.
|
||||
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
|
||||
for (i in circles) {
|
||||
var c = circles[i];
|
||||
for (let i in circles) {
|
||||
const c = circles[i];
|
||||
fb.ctxOverlay.lineWidth = c.lw;
|
||||
fb.ctxOverlay.strokeStyle = c.c;
|
||||
fb.ctxOverlay.beginPath();
|
||||
|
|
6
src/static/js/vendors/gritter.js
vendored
6
src/static/js/vendors/gritter.js
vendored
|
@ -42,8 +42,8 @@
|
|||
return Gritter.add(params || {});
|
||||
} catch(e) {
|
||||
|
||||
var err = 'Gritter Error: ' + e;
|
||||
(typeof(console) != 'undefined' && console.error) ?
|
||||
const err = 'Gritter Error: ' + e;
|
||||
(typeof(console) != 'undefined' && console.error) ?
|
||||
console.error(err, params) :
|
||||
alert(err);
|
||||
|
||||
|
@ -289,7 +289,7 @@
|
|||
*/
|
||||
_runSetup: function(){
|
||||
|
||||
for(opt in $.gritter.options){
|
||||
for(let opt in $.gritter.options){
|
||||
this[opt] = $.gritter.options[opt];
|
||||
}
|
||||
this._is_setup = 1;
|
||||
|
|
1056
src/static/js/vendors/html10n.js
vendored
1056
src/static/js/vendors/html10n.js
vendored
File diff suppressed because it is too large
Load diff
993
src/static/js/vendors/html10n.ts
vendored
Normal file
993
src/static/js/vendors/html10n.ts
vendored
Normal file
|
@ -0,0 +1,993 @@
|
|||
import {Func} from "mocha";
|
||||
|
||||
|
||||
type PluralFunc = (n: number) => string
|
||||
|
||||
export class Html10n {
|
||||
public language?: string
|
||||
private rtl: string[]
|
||||
private _pluralRules?: PluralFunc
|
||||
public mt: MicroEvent
|
||||
private loader: Loader | undefined
|
||||
public translations: Map<string, any>
|
||||
private macros: Map<string, Function>
|
||||
|
||||
constructor() {
|
||||
this.language = undefined
|
||||
this.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
|
||||
this.mt = new MicroEvent()
|
||||
this.translations = new Map()
|
||||
this.macros = new Map()
|
||||
|
||||
this.macros.set('plural', (_key: string, param:string, opts: any)=>{
|
||||
let str
|
||||
, n = parseFloat(param);
|
||||
if (isNaN(n))
|
||||
return;
|
||||
|
||||
// initialize _pluralRules
|
||||
if (this._pluralRules === undefined) {
|
||||
this._pluralRules = this.getPluralRules(this.language!);
|
||||
}
|
||||
let index = this._pluralRules!(n);
|
||||
|
||||
// try to find a [zero|one|two] key if it's defined
|
||||
if (n === 0 && ('zero') in opts) {
|
||||
str = opts['zero'];
|
||||
} else if (n == 1 && ('one') in opts) {
|
||||
str = opts['one'];
|
||||
} else if (n == 2 && ('two') in opts) {
|
||||
str = opts['two'];
|
||||
} else if (index in opts) {
|
||||
str = opts[index];
|
||||
}
|
||||
|
||||
return str;
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', ()=> {
|
||||
this.index()
|
||||
}, false)
|
||||
}
|
||||
|
||||
bind(event: string, fct: Func) {
|
||||
this.mt.bind(event, fct)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rules for plural forms (shared with JetPack), see:
|
||||
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
|
||||
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
|
||||
*
|
||||
* @param {string} lang
|
||||
* locale (language) used.
|
||||
*
|
||||
* @return {PluralFunc}
|
||||
* returns a function that gives the plural form name for a given integer:
|
||||
* var fun = getPluralRules('en');
|
||||
* fun(1) -> 'one'
|
||||
* fun(0) -> 'other'
|
||||
* fun(1000) -> 'other'.
|
||||
*/
|
||||
getPluralRules(lang: string): PluralFunc {
|
||||
const locales2rules = new Map([
|
||||
['af', 3],
|
||||
['ak', 4],
|
||||
['am', 4],
|
||||
['ar', 1],
|
||||
['asa', 3],
|
||||
['az', 0],
|
||||
['be', 11],
|
||||
['bem', 3],
|
||||
['bez', 3],
|
||||
['bg', 3],
|
||||
['bh', 4],
|
||||
['bm', 0],
|
||||
['bn', 3],
|
||||
['bo', 0],
|
||||
['br', 20],
|
||||
['brx', 3],
|
||||
['bs', 11],
|
||||
['ca', 3],
|
||||
['cgg', 3],
|
||||
['chr', 3],
|
||||
['cs', 12],
|
||||
['cy', 17],
|
||||
['da', 3],
|
||||
['de', 3],
|
||||
['dv', 3],
|
||||
['dz', 0],
|
||||
['ee', 3],
|
||||
['el', 3],
|
||||
['en', 3],
|
||||
['eo', 3],
|
||||
['es', 3],
|
||||
['et', 3],
|
||||
['eu', 3],
|
||||
['fa', 0],
|
||||
['ff', 5],
|
||||
['fi', 3],
|
||||
['fil', 4],
|
||||
['fo', 3],
|
||||
['fr', 5],
|
||||
['fur', 3],
|
||||
['fy', 3],
|
||||
['ga', 8],
|
||||
['gd', 24],
|
||||
['gl', 3],
|
||||
['gsw', 3],
|
||||
['gu', 3],
|
||||
['guw', 4],
|
||||
['gv', 23],
|
||||
['ha', 3],
|
||||
['haw', 3],
|
||||
['he', 2],
|
||||
['hi', 4],
|
||||
['hr', 11],
|
||||
['hu', 0],
|
||||
['id', 0],
|
||||
['ig', 0],
|
||||
['ii', 0],
|
||||
['is', 3],
|
||||
['it', 3],
|
||||
['iu', 7],
|
||||
['ja', 0],
|
||||
['jmc', 3],
|
||||
['jv', 0],
|
||||
['ka', 0],
|
||||
['kab', 5],
|
||||
['kaj', 3],
|
||||
['kcg', 3],
|
||||
['kde', 0],
|
||||
['kea', 0],
|
||||
['kk', 3],
|
||||
['kl', 3],
|
||||
['km', 0],
|
||||
['kn', 0],
|
||||
['ko', 0],
|
||||
['ksb', 3],
|
||||
['ksh', 21],
|
||||
['ku', 3],
|
||||
['kw', 7],
|
||||
['lag', 18],
|
||||
['lb', 3],
|
||||
['lg', 3],
|
||||
['ln', 4],
|
||||
['lo', 0],
|
||||
['lt', 10],
|
||||
['lv', 6],
|
||||
['mas', 3],
|
||||
['mg', 4],
|
||||
['mk', 16],
|
||||
['ml', 3],
|
||||
['mn', 3],
|
||||
['mo', 9],
|
||||
['mr', 3],
|
||||
['ms', 0],
|
||||
['mt', 15],
|
||||
['my', 0],
|
||||
['nah', 3],
|
||||
['naq', 7],
|
||||
['nb', 3],
|
||||
['nd', 3],
|
||||
['ne', 3],
|
||||
['nl', 3],
|
||||
['nn', 3],
|
||||
['no', 3],
|
||||
['nr', 3],
|
||||
['nso', 4],
|
||||
['ny', 3],
|
||||
['nyn', 3],
|
||||
['om', 3],
|
||||
['or', 3],
|
||||
['pa', 3],
|
||||
['pap', 3],
|
||||
['pl', 13],
|
||||
['ps', 3],
|
||||
['pt', 3],
|
||||
['rm', 3],
|
||||
['ro', 9],
|
||||
['rof', 3],
|
||||
['ru', 11],
|
||||
['rwk', 3],
|
||||
['sah', 0],
|
||||
['saq', 3],
|
||||
['se', 7],
|
||||
['seh', 3],
|
||||
['ses', 0],
|
||||
['sg', 0],
|
||||
['sh', 11],
|
||||
['shi', 19],
|
||||
['sk', 12],
|
||||
['sl', 14],
|
||||
['sma', 7],
|
||||
['smi', 7],
|
||||
['smj', 7],
|
||||
['smn', 7],
|
||||
['sms', 7],
|
||||
['sn', 3],
|
||||
['so', 3],
|
||||
['sq', 3],
|
||||
['sr', 11],
|
||||
['ss', 3],
|
||||
['ssy', 3],
|
||||
['st', 3],
|
||||
['sv', 3],
|
||||
['sw', 3],
|
||||
['syr', 3],
|
||||
['ta', 3],
|
||||
['te', 3],
|
||||
['teo', 3],
|
||||
['th', 0],
|
||||
['ti', 4],
|
||||
['tig', 3],
|
||||
['tk', 3],
|
||||
['tl', 4],
|
||||
['tn', 3],
|
||||
['to', 0],
|
||||
['tr', 0],
|
||||
['ts', 3],
|
||||
['tzm', 22],
|
||||
['uk', 11],
|
||||
['ur', 3],
|
||||
['ve', 3],
|
||||
['vi', 0],
|
||||
['vun', 3],
|
||||
['wa', 4],
|
||||
['wae', 3],
|
||||
['wo', 0],
|
||||
['xh', 3],
|
||||
['xog', 3],
|
||||
['yo', 0],
|
||||
['zh', 0],
|
||||
['zu', 3]
|
||||
])
|
||||
|
||||
function isIn(n: number, list: number[]) {
|
||||
return list.indexOf(n) !== -1;
|
||||
}
|
||||
function isBetween(n: number, start: number, end: number) {
|
||||
return start <= n && n <= end;
|
||||
}
|
||||
|
||||
type PluralFunc = (n: number) => string
|
||||
|
||||
|
||||
const pluralRules: {
|
||||
[key: string]: PluralFunc
|
||||
} = {
|
||||
'0': function() {
|
||||
return 'other';
|
||||
},
|
||||
'1': function(n: number) {
|
||||
if ((isBetween((n % 100), 3, 10)))
|
||||
return 'few';
|
||||
if (n === 0)
|
||||
return 'zero';
|
||||
if ((isBetween((n % 100), 11, 99)))
|
||||
return 'many';
|
||||
if (n == 2)
|
||||
return 'two';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'2': function(n: number) {
|
||||
if (n !== 0 && (n % 10) === 0)
|
||||
return 'many';
|
||||
if (n == 2)
|
||||
return 'two';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'3': function(n: number) {
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'4': function(n: number) {
|
||||
if ((isBetween(n, 0, 1)))
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'5': function(n: number) {
|
||||
if ((isBetween(n, 0, 2)) && n != 2)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'6': function(n: number) {
|
||||
if (n === 0)
|
||||
return 'zero';
|
||||
if ((n % 10) == 1 && (n % 100) != 11)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'7': function(n: number) {
|
||||
if (n == 2)
|
||||
return 'two';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'8': function(n: number) {
|
||||
if ((isBetween(n, 3, 6)))
|
||||
return 'few';
|
||||
if ((isBetween(n, 7, 10)))
|
||||
return 'many';
|
||||
if (n == 2)
|
||||
return 'two';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'9': function(n: number) {
|
||||
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
|
||||
return 'few';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'10': function(n: number) {
|
||||
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
|
||||
return 'few';
|
||||
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'11': function(n: number) {
|
||||
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
|
||||
return 'few';
|
||||
if ((n % 10) === 0 ||
|
||||
(isBetween((n % 10), 5, 9)) ||
|
||||
(isBetween((n % 100), 11, 14)))
|
||||
return 'many';
|
||||
if ((n % 10) == 1 && (n % 100) != 11)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'12': function(n: number) {
|
||||
if ((isBetween(n, 2, 4)))
|
||||
return 'few';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'13': function(n: number) {
|
||||
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
|
||||
return 'few';
|
||||
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
|
||||
(isBetween((n % 10), 5, 9)) ||
|
||||
(isBetween((n % 100), 12, 14)))
|
||||
return 'many';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'14': function(n: number) {
|
||||
if ((isBetween((n % 100), 3, 4)))
|
||||
return 'few';
|
||||
if ((n % 100) == 2)
|
||||
return 'two';
|
||||
if ((n % 100) == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'15': function(n: number) {
|
||||
if (n === 0 || (isBetween((n % 100), 2, 10)))
|
||||
return 'few';
|
||||
if ((isBetween((n % 100), 11, 19)))
|
||||
return 'many';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'16': function(n: number) {
|
||||
if ((n % 10) == 1 && n != 11)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'17': function(n: number) {
|
||||
if (n == 3)
|
||||
return 'few';
|
||||
if (n === 0)
|
||||
return 'zero';
|
||||
if (n == 6)
|
||||
return 'many';
|
||||
if (n == 2)
|
||||
return 'two';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'18': function(n: number) {
|
||||
if (n === 0)
|
||||
return 'zero';
|
||||
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'19': function(n: number) {
|
||||
if ((isBetween(n, 2, 10)))
|
||||
return 'few';
|
||||
if ((isBetween(n, 0, 1)))
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'20': function(n: number) {
|
||||
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
|
||||
isBetween((n % 100), 10, 19) ||
|
||||
isBetween((n % 100), 70, 79) ||
|
||||
isBetween((n % 100), 90, 99)
|
||||
))
|
||||
return 'few';
|
||||
if ((n % 1000000) === 0 && n !== 0)
|
||||
return 'many';
|
||||
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
|
||||
return 'two';
|
||||
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'21': function(n: number) {
|
||||
if (n === 0)
|
||||
return 'zero';
|
||||
if (n == 1)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'22': function(n: number) {
|
||||
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'23': function(n: number) {
|
||||
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
|
||||
return 'one';
|
||||
return 'other';
|
||||
},
|
||||
'24': function(n: number) {
|
||||
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
|
||||
return 'few';
|
||||
if (isIn(n, [2, 12]))
|
||||
return 'two';
|
||||
if (isIn(n, [1, 11]))
|
||||
return 'one';
|
||||
return 'other';
|
||||
}
|
||||
};
|
||||
|
||||
const index = locales2rules.get(lang.replace(/-.*$/, ''));
|
||||
// @ts-ignore
|
||||
if (!(index in pluralRules)) {
|
||||
console.warn('plural form unknown for [' + lang + ']');
|
||||
return function() { return 'other'; };
|
||||
}
|
||||
// @ts-ignore
|
||||
return pluralRules[index];
|
||||
}
|
||||
|
||||
getTranslatableChildren(element: HTMLElement) {
|
||||
return element.querySelectorAll('*[data-l10n-id]')
|
||||
}
|
||||
|
||||
localize(langs: (string|undefined)[]|string) {
|
||||
console.log('Available langs ', langs)
|
||||
if ('string' === typeof langs) {
|
||||
langs = [langs];
|
||||
}
|
||||
let i = 0
|
||||
langs.forEach((lang) => {
|
||||
if(!lang) return;
|
||||
langs[i++] = lang;
|
||||
if(~lang.indexOf('-')) langs[i++] = lang.substring(0, lang.indexOf('-'));
|
||||
})
|
||||
|
||||
this.build(langs, (er: null, translations: Map<string, any>) =>{
|
||||
this.translations = translations
|
||||
this.translateElement(translations)
|
||||
this.mt.trigger('localized')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the translation process
|
||||
* for an element
|
||||
* @param translations A hash of all translation strings
|
||||
* @param element A DOM element, if omitted, the document element will be used
|
||||
*/
|
||||
translateElement(translations: Map<string, any>, element?: HTMLElement) {
|
||||
element = element || document.documentElement
|
||||
const children = element ? this.getTranslatableChildren(element): document.childNodes
|
||||
|
||||
for (let child of children) {
|
||||
this.translateNode(translations, child as HTMLElement)
|
||||
}
|
||||
|
||||
// translate element itself if necessary
|
||||
this.translateNode(translations, element)
|
||||
}
|
||||
|
||||
asyncForEach(list: (string|undefined)[], iterator: any, cb: Function) {
|
||||
let i = 0
|
||||
, n = list.length
|
||||
iterator(list[i], i, function each(err?: string) {
|
||||
if(err) console.error(err)
|
||||
i++
|
||||
if (i < n) return iterator(list[i],i, each);
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a translation object from a list of langs (loads the necessary translations)
|
||||
* @param langs Array - a list of langs sorted by priority (default langs should go last)
|
||||
* @param cb Function - a callback that will be called once all langs have been loaded
|
||||
*/
|
||||
build(langs: (string|undefined)[], cb: Function) {
|
||||
const build = new Map<string, any>()
|
||||
|
||||
this.asyncForEach(langs, (lang: string, _i: number, next:LoaderFunc)=> {
|
||||
if(!lang) return next();
|
||||
this.loader!.load(lang, next)
|
||||
}, () =>{
|
||||
let lang;
|
||||
langs.reverse()
|
||||
|
||||
// loop through the priority array...
|
||||
for (let i=0, n=langs.length; i < n; i++) {
|
||||
lang = langs[i]
|
||||
if(!lang) continue;
|
||||
if(!(lang in langs)) {// uh, we don't have this lang availbable..
|
||||
// then check for related langs
|
||||
if(~lang.indexOf('-') != -1) {
|
||||
lang = lang.split('-')[0];
|
||||
}
|
||||
let l: string|undefined = ''
|
||||
for(l of langs) {
|
||||
if(l && lang != l && l.indexOf(lang) === 0) {
|
||||
lang = l
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if(lang != l) continue;
|
||||
}
|
||||
|
||||
|
||||
// ... and apply all strings of the current lang in the list
|
||||
// to our build object
|
||||
//lang = "de"
|
||||
if (this.loader!.langs.has(lang)) {
|
||||
for (let string in this.loader!.langs.get(lang)) {
|
||||
build.set(string,this.loader!.langs.get(lang)[string])
|
||||
}
|
||||
this.language = lang
|
||||
} else {
|
||||
const loaderLang = lang.split('-')[0]
|
||||
for (let string in this.loader!.langs.get(loaderLang)) {
|
||||
build.set(string,this.loader!.langs.get(loaderLang)[string])
|
||||
}
|
||||
this.language = loaderLang
|
||||
}
|
||||
|
||||
// the last applied lang will be exposed as the
|
||||
// lang the page was translated to
|
||||
}
|
||||
cb(null, build)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language that was last applied to the translations hash
|
||||
* thus overriding most of the formerly applied langs
|
||||
*/
|
||||
getLanguage() {
|
||||
return this.language
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the direction of the language returned be html10n#getLanguage
|
||||
*/
|
||||
getDirection() {
|
||||
if(!this.language) return
|
||||
const langCode = this.language.indexOf('-') == -1? this.language : this.language.substring(0, this.language.indexOf('-'))
|
||||
return this.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Index all <link>s
|
||||
*/
|
||||
index() {
|
||||
// Find all <link>s
|
||||
const links = document.getElementsByTagName('link')
|
||||
, resources = []
|
||||
for (let i=0, n=links.length; i < n; i++) {
|
||||
if (links[i].type != 'application/l10n+json')
|
||||
continue;
|
||||
resources.push(links[i].href)
|
||||
}
|
||||
this.loader = new Loader(resources)
|
||||
this.mt.trigger('indexed')
|
||||
}
|
||||
|
||||
translateNode(translations: Map<string, any>, node: HTMLElement) {
|
||||
const str: {
|
||||
id?: string,
|
||||
args?: any,
|
||||
str?: string
|
||||
|
||||
} = {}
|
||||
|
||||
// get id
|
||||
str.id = node.getAttribute('data-l10n-id') as string
|
||||
if (!str.id) return
|
||||
|
||||
if(!translations.get(str.id)) return console.warn('Couldn\'t find translation key '+str.id)
|
||||
|
||||
// get args
|
||||
if(window.JSON) {
|
||||
str.args = JSON.parse(node.getAttribute('data-l10n-args') as string)
|
||||
}else{
|
||||
try{
|
||||
//str.args = eval(node.getAttribute('data-l10n-args') as string)
|
||||
console.error("Old eval method invoked!!")
|
||||
}catch(e) {
|
||||
console.warn('Couldn\'t parse args for '+str.id)
|
||||
}
|
||||
}
|
||||
|
||||
str.str = this.get(str.id, str.args)
|
||||
|
||||
// get attribute name to apply str to
|
||||
let prop
|
||||
, index = str.id.lastIndexOf('.')
|
||||
, attrList = // allowed attributes
|
||||
{ "title": 1
|
||||
, "innerHTML": 1
|
||||
, "alt": 1
|
||||
, "textContent": 1
|
||||
, "value": 1
|
||||
, "placeholder": 1
|
||||
}
|
||||
if (index > 0 && str.id.substring(index + 1) in attrList) {
|
||||
// an attribute has been specified (example: "my_translation_key.placeholder")
|
||||
prop = str.id.substring(index + 1)
|
||||
} else { // no attribute: assuming text content by default
|
||||
prop = document.body.textContent ? 'textContent' : 'innerText'
|
||||
}
|
||||
|
||||
// Apply translation
|
||||
if (node.children.length === 0 || prop != 'textContent') {
|
||||
// @ts-ignore
|
||||
node[prop] = str.str!
|
||||
node.setAttribute("aria-label", str.str!); // Sets the aria-label
|
||||
// The idea of the above is that we always have an aria value
|
||||
// This might be a bit of an abrupt solution but let's see how it goes
|
||||
} else {
|
||||
let children = node.childNodes,
|
||||
found = false
|
||||
let i = 0, n = children.length;
|
||||
for (; i < n; i++) {
|
||||
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent!)) {
|
||||
if (!found) {
|
||||
children[i].nodeValue = str.str!
|
||||
found = true
|
||||
} else {
|
||||
children[i].nodeValue = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
console.warn('Unexpected error: could not translate element content for key '+str.id, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(id: string, args?:any) {
|
||||
let translations = this.translations
|
||||
if(!translations) return console.warn('No translations available (yet)')
|
||||
if(!translations.get(id)) return console.warn('Could not find string '+id)
|
||||
|
||||
// apply macros
|
||||
let str = translations.get(id)
|
||||
|
||||
str = this.substMacros(id, str, args)
|
||||
|
||||
// apply args
|
||||
str = this.substArguments(str, args)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
substMacros(key: string, str:string, args:any) {
|
||||
let regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
|
||||
, match
|
||||
|
||||
while(match = regex.exec(str)) {
|
||||
// a macro has been found
|
||||
// Note: at the moment, only one parameter is supported
|
||||
let macroName = match[1]
|
||||
, paramName = match[2]
|
||||
, optv = match[3]
|
||||
, opts: {[key:string]:any} = {}
|
||||
|
||||
if (!(this.macros.has(macroName))) continue
|
||||
|
||||
if(optv) {
|
||||
optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g)!.forEach(function(arg) {
|
||||
const parts = arg.split(':')
|
||||
, name = parts[0];
|
||||
opts[name] = parts[1].trim()
|
||||
})
|
||||
}
|
||||
|
||||
let param
|
||||
if (args && paramName in args) {
|
||||
param = args[paramName]
|
||||
} else if (paramName in this.translations) {
|
||||
param = this.translations.get(paramName)
|
||||
}
|
||||
|
||||
// there's no macro parser: it has to be defined in html10n.macros
|
||||
let macro = this.macros.get(macroName)!
|
||||
str = str.substring(0, match.index) + macro(key, param, opts) + str.substring(match.index+match[0].length)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
substArguments(str: string, args:any) {
|
||||
let reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
|
||||
, match
|
||||
let translations = this.translations;
|
||||
while (match = reArgs.exec(str)) {
|
||||
if (!match || match.length < 2)
|
||||
return str // argument key not found
|
||||
|
||||
let arg = match[1]
|
||||
, sub = ''
|
||||
if (args && arg in args) {
|
||||
sub = args[arg]
|
||||
} else if (translations && arg in translations) {
|
||||
sub = translations.get(arg)
|
||||
} else {
|
||||
console.warn('Could not find argument {{' + arg + '}}')
|
||||
return str
|
||||
}
|
||||
|
||||
str = str.substring(0, match.index) + sub + str.substring(match.index + match[0].length)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class MicroEvent {
|
||||
private events: Map<string, Function[]>
|
||||
|
||||
constructor() {
|
||||
this.events = new Map();
|
||||
}
|
||||
|
||||
bind(event: string, fct: Func) {
|
||||
if (this.events.get(event) === undefined) {
|
||||
this.events.set(event, []);
|
||||
}
|
||||
|
||||
this.events.get(event)!.push(fct);
|
||||
}
|
||||
|
||||
unbind(event: string, fct: Func) {
|
||||
if (this.events.get(event) === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.events.get(event)!.indexOf(fct);
|
||||
if (index !== -1) {
|
||||
this.events.get(event)!.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
trigger(event: string, ...args: any[]) {
|
||||
if (this.events.get(event) === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const fct of this.events.get(event)!) {
|
||||
fct(...args);
|
||||
}
|
||||
}
|
||||
|
||||
mixin(destObject: any) {
|
||||
const props = ['bind', 'unbind', 'trigger'];
|
||||
if (destObject !== undefined) {
|
||||
for (const prop of props) {
|
||||
// @ts-ignore
|
||||
destObject[prop] = this[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type LoaderFunc = () => void
|
||||
|
||||
type ErrorFunc = (data?:any)=>void
|
||||
|
||||
class Loader {
|
||||
private resources: any
|
||||
private cache: Map<string, any>
|
||||
langs: Map<string, any>
|
||||
|
||||
constructor(resources: any) {
|
||||
this.resources = resources;
|
||||
this.cache = new Map();
|
||||
this.langs = new Map();
|
||||
}
|
||||
|
||||
load(lang: string, callback: LoaderFunc) {
|
||||
if (this.langs.get(lang) !== undefined) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.resources.length > 0) {
|
||||
let reqs = 0
|
||||
for (const resource of this.resources) {
|
||||
this.fetch(resource, lang, (e)=> {
|
||||
reqs++;
|
||||
if (e) console.warn(e)
|
||||
|
||||
if (reqs < this.resources.length) return;// Call back once all reqs are completed
|
||||
callback && callback()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(href: string, lang: string, callback: ErrorFunc) {
|
||||
|
||||
if (this.cache.get(href)) {
|
||||
this.parse(lang, href, this.cache.get(href), callback)
|
||||
return;
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', href, /*async: */true)
|
||||
if (xhr.overrideMimeType) {
|
||||
xhr.overrideMimeType('application/json; charset=utf-8');
|
||||
}
|
||||
xhr.onreadystatechange = ()=> {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200 || xhr.status === 0) {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
this.cache.set(href, data)
|
||||
// Pass on the contents for parsing
|
||||
this.parse(lang, href, data, callback)
|
||||
} else {
|
||||
callback(new Error('Failed to load '+href))
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
|
||||
parse(lang: string, href: string, data: {
|
||||
[key: string]: string
|
||||
}, callback: ErrorFunc) {
|
||||
if ('object' !== typeof data) {
|
||||
callback(new Error('A file couldn\'t be parsed as json.'))
|
||||
return
|
||||
}
|
||||
|
||||
function getBcp47LangCode(browserLang: string) {
|
||||
const bcp47Lang = browserLang.toLowerCase();
|
||||
|
||||
// Browser => BCP 47
|
||||
const langCodeMap = new Map([
|
||||
['zh-cn', 'zh-hans-cn'],
|
||||
['zh-hk', 'zh-hant-hk'],
|
||||
['zh-mo', 'zh-hant-mo'],
|
||||
['zh-my', 'zh-hans-my'],
|
||||
['zh-sg', 'zh-hans-sg'],
|
||||
['zh-tw', 'zh-hant-tw'],
|
||||
])
|
||||
|
||||
return langCodeMap.get(bcp47Lang) ?? bcp47Lang;
|
||||
}
|
||||
|
||||
// Issue #6129: Fix exceptions
|
||||
// NOTE: translatewiki.net use all lowercase form by default ('en-gb' insted of 'en-GB')
|
||||
function getJsonLangCode(bcp47Lang: string) {
|
||||
const jsonLang = bcp47Lang.toLowerCase();
|
||||
// BCP 47 => JSON
|
||||
const langCodeMap = new Map([
|
||||
['sr-ec', 'sr-cyrl'],
|
||||
['sr-el', 'sr-latn'],
|
||||
['zh-hk', 'zh-hant-hk'],
|
||||
])
|
||||
|
||||
return langCodeMap.get(jsonLang) ?? jsonLang;
|
||||
}
|
||||
|
||||
let bcp47LangCode = getBcp47LangCode(lang);
|
||||
let jsonLangCode = getJsonLangCode(bcp47LangCode);
|
||||
|
||||
if (!data[jsonLangCode]) {
|
||||
// lang not found
|
||||
// This may be due to formatting (expected 'ru' but browser sent 'ru-RU')
|
||||
// Set err msg before mutating lang (we may need this later)
|
||||
const msg = 'Couldn\'t find translations for ' + lang +
|
||||
'(lowercase BCP 47 lang tag ' + bcp47LangCode +
|
||||
', JSON lang code ' + jsonLangCode + ')';
|
||||
// Check for '-' (BCP 47 'ROOT-SCRIPT-REGION-VARIANT') and fallback until found data or ROOT
|
||||
// - 'ROOT-SCRIPT-REGION': 'zh-Hans-CN'
|
||||
// - 'ROOT-SCRIPT': 'zh-Hans'
|
||||
// - 'ROOT-REGION': 'en-GB'
|
||||
// - 'ROOT-VARIANT': 'be-tarask'
|
||||
while (!data[jsonLangCode] && bcp47LangCode.lastIndexOf('-') > -1) {
|
||||
// ROOT-SCRIPT-REGION-VARIANT formatting detected
|
||||
bcp47LangCode = bcp47LangCode.substring(0, bcp47LangCode.lastIndexOf('-')); // set lang to ROOT lang
|
||||
jsonLangCode = getJsonLangCode(bcp47LangCode);
|
||||
}
|
||||
|
||||
if (!data[jsonLangCode]) {
|
||||
// ROOT lang not found. (e.g 'zh')
|
||||
// Loop through langs data. Maybe we have a variant? e.g (zh-hans)
|
||||
let l; // langs item. Declare outside of loop
|
||||
|
||||
for (l in data) {
|
||||
// Is not ROOT?
|
||||
// And is variant of ROOT?
|
||||
// (NOTE: index of ROOT equals 0 would cause unexpected ISO 639-1 vs. 639-3 issues,
|
||||
// so append dash into query string)
|
||||
// And is known lang?
|
||||
if (bcp47LangCode != l && l.indexOf(lang + '-') === 0 && data[l]) {
|
||||
bcp47LangCode = l; // set lang to ROOT-SCRIPT (e.g 'zh-hans')
|
||||
jsonLangCode = getJsonLangCode(bcp47LangCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Did we find a variant? If not, return err.
|
||||
if (bcp47LangCode != l) {
|
||||
return callback(new Error(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lang = jsonLangCode
|
||||
|
||||
if('string' === typeof data[lang]) {
|
||||
// Import rule
|
||||
|
||||
// absolute path
|
||||
let importUrl = data[lang];
|
||||
|
||||
// relative path
|
||||
if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
|
||||
importUrl = href+"/../"+data[lang]
|
||||
}
|
||||
|
||||
this.fetch(importUrl, lang, callback)
|
||||
return
|
||||
}
|
||||
|
||||
if ('object' != typeof data[lang]) {
|
||||
callback(new Error('Translations should be specified as JSON objects!'))
|
||||
return
|
||||
}
|
||||
|
||||
this.langs.set(lang,data[lang])
|
||||
// TODO: Also store accompanying langs
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
export default new Html10n()
|
8
src/static/js/vendors/nice-select.js
vendored
8
src/static/js/vendors/nice-select.js
vendored
|
@ -110,10 +110,10 @@
|
|||
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
|
||||
}
|
||||
|
||||
$listHeight = $dropdown.find('.list').outerHeight();
|
||||
$top = $dropdown.parent().offset().top;
|
||||
$bottom = $('body').height() - $top;
|
||||
$maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
||||
let $listHeight = $dropdown.find('.list').outerHeight();
|
||||
let $top = $dropdown.parent().offset().top;
|
||||
let $bottom = $('body').height() - $top;
|
||||
let $maxListHeight = $bottom - $dropdown.outerHeight() - 20;
|
||||
if ($maxListHeight < 200) {
|
||||
$dropdown.addClass('reverse');
|
||||
$maxListHeight = 250;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue