etherpad-lite/src/static/js/ace.js

283 lines
9.8 KiB
JavaScript
Raw Normal View History

2021-01-29 00:43:53 -05:00
'use strict';
/**
2013-06-15 01:37:41 +08:00
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
2011-03-26 13:10:41 +00:00
/**
* Copyright 2009 Google Inc.
2011-07-07 18:59:34 +01:00
*
2011-03-26 13:10:41 +00:00
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
2011-07-07 18:59:34 +01:00
*
2011-03-26 13:10:41 +00:00
* http://www.apache.org/licenses/LICENSE-2.0
2011-07-07 18:59:34 +01:00
*
2011-03-26 13:10:41 +00:00
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// requires: top
// requires: undefined
2020-11-23 13:24:19 -05:00
const hooks = require('./pluginfw/hooks');
const pluginUtils = require('./pluginfw/shared');
2021-01-29 00:43:53 -05:00
const Ace2Editor = function () {
2020-11-23 13:24:19 -05:00
const ace2 = Ace2Editor;
2011-03-26 13:10:41 +00:00
2020-11-23 13:24:19 -05:00
let info = {
2021-01-29 00:43:53 -05:00
editor: this,
2020-11-23 13:24:19 -05:00
id: (ace2.registry.nextId++),
2011-07-07 18:59:34 +01:00
};
2020-11-23 13:24:19 -05:00
let loaded = false;
2011-03-26 13:10:41 +00:00
2020-11-23 13:24:19 -05:00
let actionsPendingInit = [];
2011-07-07 18:59:34 +01:00
const pendingInit = (func) => function (...args) {
const action = () => func.apply(this, args);
if (loaded) return action();
actionsPendingInit.push(action);
};
2011-07-07 18:59:34 +01:00
2021-01-29 00:43:53 -05:00
const doActionsPendingInit = () => {
for (const fn of actionsPendingInit) fn();
2011-03-26 13:10:41 +00:00
actionsPendingInit = [];
2021-01-29 00:43:53 -05:00
};
2013-06-15 01:37:41 +08:00
2011-03-26 13:10:41 +00:00
ace2.registry[info.id] = info;
// The following functions (prefixed by 'ace_') are exposed by editor, but
// execution is delayed until init is complete
2021-01-29 00:43:53 -05:00
const aceFunctionsPendingInit = [
'importText',
2020-11-23 13:24:19 -05:00
'importAText',
'focus',
'setEditable',
'getFormattedCode',
'setOnKeyPress',
'setOnKeyDown',
'setNotifyDirty',
'setProperty',
'setBaseText',
'setBaseAttributedText',
'applyChangesToBase',
'applyPreparedChangesetToBase',
'setUserChangeNotificationCallback',
'setAuthorInfo',
'setAuthorSelectionRange',
'callWithAce',
'execCommand',
2021-01-29 00:43:53 -05:00
'replaceRange',
];
2020-11-23 13:24:19 -05:00
for (const fnName of aceFunctionsPendingInit) {
// Note: info[`ace_${fnName}`] does not exist yet, so it can't be passed directly to
// pendingInit(). A simple wrapper is used to defer the info[`ace_${fnName}`] lookup until
// method invocation.
2021-01-29 00:43:53 -05:00
this[fnName] = pendingInit(function (...args) {
info[`ace_${fnName}`].apply(this, args);
});
}
2013-06-15 01:37:41 +08:00
2021-01-29 00:43:53 -05:00
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
2013-06-15 01:37:41 +08:00
2021-01-29 00:43:53 -05:00
this.getFrame = () => info.frame || null;
2013-06-15 01:37:41 +08:00
2021-01-29 00:43:53 -05:00
this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop);
2011-07-07 18:59:34 +01:00
2021-01-29 00:43:53 -05:00
this.getInInternationalComposition =
() => loaded ? info.ace_getInInternationalComposition() : false;
2011-03-26 13:10:41 +00:00
// prepareUserChangeset:
// Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes
// to the latest base text into a Changeset, which is returned (as a string if encodeAsString).
2021-01-29 00:43:53 -05:00
// If this method returns a truthy value, then applyPreparedChangesetToBase can be called at some
// later point to consider these changes part of the base, after which prepareUserChangeset must
// be called again before applyPreparedChangesetToBase. Multiple consecutive calls to
// prepareUserChangeset will return an updated changeset that takes into account the latest user
// changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly.
this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null;
2011-03-26 13:10:41 +00:00
2021-01-29 00:43:53 -05:00
// returns array of {error: <browser Error object>, time: +new Date()}
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
2011-03-26 13:10:41 +00:00
2021-01-29 00:43:53 -05:00
const sortFilesByEmbeded = (files) => {
2020-11-23 13:24:19 -05:00
const embededFiles = [];
let remoteFiles = [];
2012-01-15 00:05:26 -08:00
if (Ace2Editor.EMBEDED) {
2020-11-23 13:24:19 -05:00
for (let i = 0, ii = files.length; i < ii; i++) {
const file = files[i];
2012-01-15 00:05:26 -08:00
if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) {
embededFiles.push(file);
} else {
remoteFiles.push(file);
}
}
} else {
2012-01-15 00:05:26 -08:00
remoteFiles = files;
}
2012-01-15 00:05:26 -08:00
return {embeded: embededFiles, remote: remoteFiles};
2021-01-29 00:43:53 -05:00
};
const addStyleTagsFor = (doc, files) => {
2020-11-23 13:24:19 -05:00
const sorted = sortFilesByEmbeded(files);
const embededFiles = sorted.embeded;
const remoteFiles = sorted.remote;
2012-01-15 00:05:26 -08:00
if (embededFiles.length > 0) {
const css = embededFiles.map((f) => Ace2Editor.EMBEDED[f]).join('\n');
const style = doc.createElement('style');
style.type = 'text/css';
style.appendChild(doc.createTextNode(css));
doc.head.appendChild(style);
2012-01-15 00:05:26 -08:00
}
2021-01-29 00:43:53 -05:00
for (const file of remoteFiles) {
const link = doc.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = encodeURI(file);
doc.head.appendChild(link);
2012-01-15 00:05:26 -08:00
}
2021-01-29 00:43:53 -05:00
};
2011-03-26 13:10:41 +00:00
2021-01-29 00:43:53 -05:00
this.destroy = pendingInit(() => {
2011-03-26 13:10:41 +00:00
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
delete ace2.registry[info.id];
info = null; // prevent IE 6 closure memory leaks
});
2021-01-29 00:43:53 -05:00
this.init = function (containerId, initialCode, doneFunc) {
this.importText(initialCode);
2011-03-26 13:10:41 +00:00
2021-01-29 00:43:53 -05:00
info.onEditorReady = () => {
2011-03-26 13:10:41 +00:00
loaded = true;
doActionsPendingInit();
doneFunc();
};
const includedCSS = [
'../static/css/iframe_editor.css',
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
// Allow urls to external CSS - http(s):// and //some/path.css
...hooks.callAll('aceEditorCSS').map((p) => /\/\//.test(p) ? p : `../static/plugins/${p}`),
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
];
2013-06-15 01:37:41 +08:00
const outerFrame = document.createElement('iframe');
2021-02-26 01:37:33 -05:00
outerFrame.name = 'ace_outer';
outerFrame.frameBorder = 0; // for IE
outerFrame.title = 'Ether';
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);
const outerWindow = outerFrame.contentWindow;
const outerDocument = outerWindow.document;
const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== '');
outerDocument.documentElement.classList.add('inner-editor', 'outerdoc', ...skinVariants);
addStyleTagsFor(outerDocument, includedCSS);
const style = outerDocument.createElement('style');
style.type = 'text/css';
style.title = 'dynamicsyntax';
outerDocument.head.appendChild(style);
const link = outerDocument.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'data:text/css,';
outerDocument.head.appendChild(link);
outerWindow.editorInfo = Ace2Editor.registry[info.id];
outerWindow.onload = () => {
outerWindow.onload = null;
const window = outerWindow;
const document = outerDocument;
setTimeout(() => {
const iframe = document.createElement('iframe');
iframe.name = 'ace_inner';
iframe.title = 'pad';
iframe.scrolling = 'no';
iframe.frameBorder = 0;
iframe.allowTransparency = true; // for IE
iframe.ace_outerWin = window;
document.body.insertBefore(iframe, document.body.firstChild);
window.readyFunc = () => {
delete window.readyFunc;
window.editorInfo.onEditorReady();
delete window.editorInfo;
};
const innerWindow = iframe.contentWindow;
const innerDocument = innerWindow.document;
innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
addStyleTagsFor(innerDocument, includedCSS);
const script = innerDocument.createElement('script');
script.type = 'text/javascript';
script.src = `../static/js/require-kernel.js?v=${clientVars.randomVersionString}`;
innerDocument.head.appendChild(script);
innerWindow.onload = () => {
innerWindow.onload = null;
const window = innerWindow;
const require = window.require;
require.setRootURI('../javascripts/src');
require.setLibraryURI('../javascripts/lib');
require.setGlobalKeyPath('require');
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
window.plugins.adoptPluginsFromAncestorsOf(window);
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
window.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
window.plugins.ensure(() => window.Ace2Inner.init());
};
const style = innerDocument.createElement('style');
style.type = 'text/css';
style.title = 'dynamicsyntax';
innerDocument.head.appendChild(style);
const headLines = [];
hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines});
const tmp = innerDocument.createElement('div');
tmp.innerHTML = headLines.join('\n');
while (tmp.firstChild) innerDocument.head.appendChild(tmp.firstChild);
innerDocument.body.id = 'innerdocbody';
innerDocument.body.classList.add('innerdocbody');
innerDocument.body.setAttribute('role', 'application');
innerDocument.body.setAttribute('spellcheck', 'false');
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp;
}, 0);
};
outerDocument.body.id = 'outerdocbody';
outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames());
const sideDiv = outerDocument.createElement('div');
sideDiv.id = 'sidediv';
sideDiv.classList.add('sidediv');
outerDocument.body.appendChild(sideDiv);
2021-02-26 01:37:33 -05:00
const lineMetricsDiv = outerDocument.createElement('div');
lineMetricsDiv.id = 'linemetricsdiv';
lineMetricsDiv.appendChild(outerDocument.createTextNode('x'));
outerDocument.body.appendChild(lineMetricsDiv);
2011-03-26 13:10:41 +00:00
};
2021-01-29 00:43:53 -05:00
};
2011-03-26 13:10:41 +00:00
2021-01-29 00:43:53 -05:00
Ace2Editor.registry = {
nextId: 1,
};
exports.Ace2Editor = Ace2Editor;