Added more frontend ts files

This commit is contained in:
SamTv12345 2024-07-19 19:22:04 +02:00
parent cef2af15b9
commit fa2d6d15a9
37 changed files with 2871 additions and 2534 deletions

8
pnpm-lock.yaml generated
View file

@ -324,6 +324,9 @@ importers:
'@types/underscore': '@types/underscore':
specifier: ^1.11.15 specifier: ^1.11.15
version: 1.11.15 version: 1.11.15
'@types/unorm':
specifier: ^1.3.31
version: 1.3.31
chokidar: chokidar:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
@ -1612,6 +1615,9 @@ packages:
'@types/unist@3.0.2': '@types/unist@3.0.2':
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
'@types/unorm@1.3.31':
resolution: {integrity: sha512-qCPX/Lo14ECb9Wkb/1sxdcTQqIiHTVNlaHczGrh2WqMVSlWjfn8Hu7DxraCtBYz1+Ud6Id/d+4OH/hkd+dlnpw==}
'@types/url-join@4.0.3': '@types/url-join@4.0.3':
resolution: {integrity: sha512-3l1qMm3wqO0iyC5gkADzT95UVW7C/XXcdvUcShOideKF0ddgVRErEQQJXBd2kvQm+aSgqhBGHGB38TgMeT57Ww==} resolution: {integrity: sha512-3l1qMm3wqO0iyC5gkADzT95UVW7C/XXcdvUcShOideKF0ddgVRErEQQJXBd2kvQm+aSgqhBGHGB38TgMeT57Ww==}
@ -5603,6 +5609,8 @@ snapshots:
'@types/unist@3.0.2': {} '@types/unist@3.0.2': {}
'@types/unorm@1.3.31': {}
'@types/url-join@4.0.3': {} '@types/url-join@4.0.3': {}
'@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.20': {}

View file

@ -1,4 +1,5 @@
import {MapArrayType} from "./MapType"; import {MapArrayType} from "./MapType";
import {PadOption} from "../../static/js/types/SocketIOMessage";
export type PadType = { export type PadType = {
id: string, id: string,
@ -19,7 +20,7 @@ export type PadType = {
getRevisionDate: (rev: number)=>Promise<number>, getRevisionDate: (rev: number)=>Promise<number>,
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>, getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,
appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>, appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>,
settings:any settings: PadOption
} }

View file

@ -97,6 +97,7 @@
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/underscore": "^1.11.15", "@types/underscore": "^1.11.15",
"@types/unorm": "^1.3.31",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-config-etherpad": "^4.0.4", "eslint-config-etherpad": "^4.0.4",

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import AttributePool from "./AttributePool"; import AttributePool from "./AttributePool";
import {Attribute} from "./types/Attribute";
const attributes = require('./attributes'); const attributes = require('./attributes');
@ -66,7 +67,7 @@ class AttributeMap extends Map {
* key is removed from this map (if present). * key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining). * @returns {AttributeMap} `this` (for chaining).
*/ */
update(entries: Iterable<[string, string]>, emptyValueIsDelete: boolean = false): AttributeMap { update(entries: Attribute[], emptyValueIsDelete: boolean = false): AttributeMap {
for (let [k, v] of entries) { for (let [k, v] of entries) {
k = k == null ? '' : String(k); k = k == null ? '' : String(k);
v = v == null ? '' : String(v); v = v == null ? '' : String(v);

View file

@ -1107,7 +1107,7 @@ export const attribsAttributeValue = (attribs: string, key: string, pool: Attrib
* ignored if `attribs` is an attribute string. * ignored if `attribs` is an attribute string.
* @returns {AttributeString} * @returns {AttributeString}
*/ */
export const makeAttribsString = (opcode: string, attribs: Iterable<[string, string]>|string, pool: AttributePool | null | undefined): string => { export const makeAttribsString = (opcode: string, attribs: Attribute[]|string, pool: AttributePool | null | undefined): string => {
padutils.warnDeprecated( padutils.warnDeprecated(
'Changeset.makeAttribsString() is deprecated; ' + 'Changeset.makeAttribsString() is deprecated; ' +
'use AttributeMap.prototype.toString() or attributes.attribsToString() instead'); 'use AttributeMap.prototype.toString() or attributes.attribsToString() instead');

View file

@ -9,11 +9,11 @@ import {padUtils} from './pad_utils'
* Supports serialization to JSON. * Supports serialization to JSON.
*/ */
class ChatMessage { class ChatMessage {
customMetadata: any
private text: string|null text: string|null
private authorId: string|null public authorId: string|null
private displayName: string|null private displayName: string|null
private time: number|null time: number|null
static fromObject(obj: ChatMessage) { static fromObject(obj: ChatMessage) {
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept // The userId property was renamed to authorId, and userName was renamed to displayName. Accept
// the old names in case the db record was written by an older version of Etherpad. // the old names in case the db record was written by an older version of Etherpad.
@ -108,4 +108,4 @@ class ChatMessage {
} }
} }
module.exports = ChatMessage; export default ChatMessage

View file

@ -6,6 +6,8 @@
*/ */
import {InnerWindow} from "./types/InnerWindow"; import {InnerWindow} from "./types/InnerWindow";
import {AText} from "./types/AText";
import AttributePool from "./AttributePool";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
@ -30,7 +32,8 @@ const hooks = require('./pluginfw/hooks');
const pluginUtils = require('./pluginfw/shared'); const pluginUtils = require('./pluginfw/shared');
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner') const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
const debugLog = (...args: string[]|Object[]|null[]) => {}; const debugLog = (...args: string[] | Object[] | null[]) => {
};
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins') const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
const {Cssmanager} = require("./cssmanager"); const {Cssmanager} = require("./cssmanager");
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that. // The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
@ -58,7 +61,8 @@ const eventFired = async (obj: any, event: string, cleanups: Function[] = [], pr
reject(err); reject(err);
}; };
cleanup = () => { cleanup = () => {
cleanup = () => {}; cleanup = () => {
};
obj!.removeEventListener(event, successCb); obj!.removeEventListener(event, successCb);
obj!.removeEventListener('error', errorCb); obj!.removeEventListener('error', errorCb);
}; };
@ -90,6 +94,26 @@ const frameReady = async (frame: HTMLIFrameElement) => {
}; };
export class Ace2Editor { export class Ace2Editor {
callWithAce(arg0: (ace: any) => void, cmd?: string, flag?: boolean) {
throw new Error("Method not implemented.");
}
focus = () => {
}
setEditable = (editable: boolean)=>{
}
importAText = (atext: AText, apool: AttributePool, flag: boolean)=>{
}
setProperty = (ev: string, padFontFam: string|boolean)=>{
}
info = {editor: this}; info = {editor: this};
loaded = false; loaded = false;
actionsPendingInit: Function[] = []; actionsPendingInit: Function[] = [];
@ -108,7 +132,7 @@ export class Ace2Editor {
} }
} }
pendingInit = (func: Function) => (...args: any[])=> { pendingInit = (func: Function) => (...args: any[]) => {
const action = () => func.apply(this, args); const action = () => func.apply(this, args);
if (this.loaded) return action(); if (this.loaded) return action();
this.actionsPendingInit.push(action); this.actionsPendingInit.push(action);
@ -176,7 +200,7 @@ export class Ace2Editor {
this.info = null; // prevent IE 6 closure memory leaks this.info = null; // prevent IE 6 closure memory leaks
}); });
init = async (containerId: string, initialCode: string)=> { init = async (containerId: string, initialCode: string) => {
debugLog('Ace2Editor.init()'); debugLog('Ace2Editor.init()');
// @ts-ignore // @ts-ignore
this.importText(initialCode); this.importText(initialCode);
@ -296,7 +320,7 @@ export class Ace2Editor {
await innerWindow.Ace2Inner.init(this.info, { await innerWindow.Ace2Inner.init(this.info, {
inner: new Cssmanager(innerStyle.sheet), inner: new Cssmanager(innerStyle.sheet),
outer: new Cssmanager(outerStyle.sheet), outer: new Cssmanager(outerStyle.sheet),
parent: new Cssmanager((document.querySelector('style[title="dynamicsyntax"]') as HTMLStyleElement)!.sheet), parent: new Cssmanager((document.querySelector('style[title="dynamicsyntax"]') as HTMLStyleElement)!.sheet),
}); });
debugLog('Ace2Editor.init() Ace2Inner.init() returned'); debugLog('Ace2Editor.init() Ace2Inner.init() returned');
this.loaded = true; this.loaded = true;

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {MapArrayType} from "../../node/types/MapType";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -22,11 +24,13 @@
* limitations under the License. * limitations under the License.
*/ */
const isNodeText = (node) => (node.nodeType === 3); export const isNodeText = (node: {
nodeType: number
}) => (node.nodeType === 3);
const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; export const getAssoc = (obj: MapArrayType<any>, name: string) => obj[`_magicdom_${name}`];
const setAssoc = (obj, name, value) => { export const setAssoc = (obj: MapArrayType<any>, name: string, value: string) => {
// note that in IE designMode, properties of a node can get // note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also, // copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste // properties representable in HTML text can survive copy-and-paste
@ -38,7 +42,7 @@ const setAssoc = (obj, name, value) => {
// between false and true, a number between 0 and numItems inclusive. // between false and true, a number between 0 and numItems inclusive.
const binarySearch = (numItems, func) => { export const binarySearch = (numItems: number, func: (num: number)=>boolean) => {
if (numItems < 1) return 0; if (numItems < 1) return 0;
if (func(0)) return 0; if (func(0)) return 0;
if (!func(numItems - 1)) return numItems; if (!func(numItems - 1)) return numItems;
@ -52,17 +56,10 @@ const binarySearch = (numItems, func) => {
return high; return high;
}; };
const binarySearchInfinite = (expectedLength, func) => { export const binarySearchInfinite = (expectedLength: number, func: (num: number)=>boolean) => {
let i = 0; let i = 0;
while (!func(i)) i += expectedLength; while (!func(i)) i += expectedLength;
return binarySearch(i, func); return binarySearch(i, func);
}; };
const noop = () => {}; export const noop = () => {};
exports.isNodeText = isNodeText;
exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch;
exports.binarySearchInfinite = binarySearchInfinite;
exports.noop = noop;

View file

@ -1,502 +0,0 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks');
const browser = require('./vendors/browser');
// Dependency fill on init. This exists for `pad.socket` only.
// TODO: bind directly to the socket.
let pad = undefined;
const getSocket = () => pad && pad.socket;
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
ACE's ready callback does not need to have fired yet.
"serverVars" are from calling doc.getCollabClientVars() on the server. */
const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) => {
const editor = ace2editor;
pad = _pad; // Inject pad to avoid a circular dependency.
let rev = serverVars.rev;
let committing = false;
let stateMessage;
let channelState = 'CONNECTING';
let lastCommitTime = 0;
let initialStartConnectTime = 0;
let commitDelay = 500;
const userId = initialUserInfo.userId;
// var socket;
const userSet = {}; // userId -> userInfo
userSet[userId] = initialUserInfo;
let isPendingRevision = false;
const callbacks = {
onUserJoin: () => {},
onUserLeave: () => {},
onUpdateUserInfo: () => {},
onChannelStateChange: () => {},
onClientMessage: () => {},
onInternalAction: () => {},
onConnectionTrouble: () => {},
onServerMessage: () => {},
};
if (browser.firefox) {
// Prevent "escape" from taking effect and canceling a comet connection;
// doesn't work if focus is on an iframe.
$(window).on('keydown', (evt) => {
if (evt.which === 27) {
evt.preventDefault();
}
});
}
const handleUserChanges = () => {
if (editor.getInInternationalComposition()) {
// handleUserChanges() will be called again once composition ends so there's no need to set up
// a future call before returning.
return;
}
const now = Date.now();
if ((!getSocket()) || channelState === 'CONNECTING') {
if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) {
setChannelState('DISCONNECTED', 'initsocketfail');
} else {
// check again in a bit
setTimeout(handleUserChanges, 1000);
}
return;
}
if (committing) {
if (now - lastCommitTime > 20000) {
// a commit is taking too long
setChannelState('DISCONNECTED', 'slowcommit');
} else if (now - lastCommitTime > 5000) {
callbacks.onConnectionTrouble('SLOW');
} else {
// run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000);
}
return;
}
const earliestCommit = lastCommitTime + commitDelay;
if (now < earliestCommit) {
setTimeout(handleUserChanges, earliestCommit - now);
return;
}
let sentMessage = false;
// Check if there are any pending revisions to be received from server.
// Allow only if there are no pending revisions to be received from server
if (!isPendingRevision) {
const userChangesData = editor.prepareUserChangeset();
if (userChangesData.changeset) {
lastCommitTime = now;
committing = true;
stateMessage = {
type: 'USER_CHANGES',
baseRev: rev,
changeset: userChangesData.changeset,
apool: userChangesData.apool,
};
sendMessage(stateMessage);
sentMessage = true;
callbacks.onInternalAction('commitPerformed');
}
} else {
// run again in a few seconds, to check if there was a reconnection attempt
setTimeout(handleUserChanges, 3000);
}
if (sentMessage) {
// run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000);
}
};
const acceptCommit = () => {
editor.applyPreparedChangesetToBase();
setStateIdle();
try {
callbacks.onInternalAction('commitAcceptedByServer');
callbacks.onConnectionTrouble('OK');
} catch (err) { /* intentionally ignored */ }
handleUserChanges();
};
const setUpSocket = () => {
setChannelState('CONNECTED');
doDeferredActions();
initialStartConnectTime = Date.now();
};
const sendMessage = (msg) => {
getSocket().emit('message',
{
type: 'COLLABROOM',
component: 'pad',
data: msg,
});
};
const serverMessageTaskQueue = new class {
constructor() {
this._promiseChain = Promise.resolve();
}
async enqueue(fn) {
const taskPromise = this._promiseChain.then(fn);
// Use .catch() to prevent rejections from halting the queue.
this._promiseChain = taskPromise.catch(() => {});
// Do NOT do `return await this._promiseChain;` because the caller would not see an error if
// fn() throws/rejects (due to the .catch() added above).
return await taskPromise;
}
}();
const handleMessageFromServer = (evt) => {
if (!getSocket()) return;
if (!evt.data) return;
const wrapper = evt;
if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM') return;
const msg = wrapper.data;
if (msg.type === 'NEW_CHANGES') {
serverMessageTaskQueue.enqueue(async () => {
// Avoid updating the DOM while the user is composing a character. Notes about this `await`:
// * `await null;` is equivalent to `await Promise.resolve(null);`, so if the user is not
// currently composing a character then execution will continue without error.
// * We assume that it is not possible for a new 'compositionstart' event to fire after
// the `await` but before the next line of code after the `await` (or, if it is
// possible, that the chances are so small or the consequences so minor that it's not
// worth addressing).
await editor.getInInternationalComposition();
const {newRev, changeset, author = '', apool} = msg;
if (newRev !== (rev + 1)) {
window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_newchanges");
return;
}
rev = newRev;
editor.applyChangesToBase(changeset, author, apool);
});
} else if (msg.type === 'ACCEPT_COMMIT') {
serverMessageTaskQueue.enqueue(() => {
const {newRev} = msg;
// newRev will equal rev if the changeset has no net effect (identity changeset, removing
// and re-adding the same characters with the same attributes, or retransmission of an
// already applied changeset).
if (![rev, rev + 1].includes(newRev)) {
window.console.warn(`bad message revision on ACCEPT_COMMIT: ${newRev} not ${rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_acceptcommit");
return;
}
rev = newRev;
acceptCommit();
});
} else if (msg.type === 'CLIENT_RECONNECT') {
// Server sends a CLIENT_RECONNECT message when there is a client reconnect.
// Server also returns all pending revisions along with this CLIENT_RECONNECT message
serverMessageTaskQueue.enqueue(() => {
if (msg.noChanges) {
// If no revisions are pending, just make everything normal
setIsPendingRevision(false);
return;
}
const {headRev, newRev, changeset, author = '', apool} = msg;
if (newRev !== (rev + 1)) {
window.console.warn(`bad message revision on CLIENT_RECONNECT: ${newRev} not ${rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_acceptcommit");
return;
}
rev = newRev;
if (author === pad.getUserId()) {
acceptCommit();
} else {
editor.applyChangesToBase(changeset, author, apool);
}
if (newRev === headRev) {
// Once we have applied all pending revisions, make everything normal
setIsPendingRevision(false);
}
});
} else if (msg.type === 'USER_NEWINFO') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (userSet[id]) {
userSet[id] = userInfo;
callbacks.onUpdateUserInfo(userInfo);
} else {
userSet[id] = userInfo;
callbacks.onUserJoin(userInfo);
}
tellAceActiveAuthorInfo(userInfo);
} else if (msg.type === 'USER_LEAVE') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (userSet[id]) {
delete userSet[userInfo.userId];
fadeAceAuthorInfo(userInfo);
callbacks.onUserLeave(userInfo);
}
} else if (msg.type === 'CLIENT_MESSAGE') {
callbacks.onClientMessage(msg.payload);
} else if (msg.type === 'CHAT_MESSAGE') {
chat.addMessage(msg.message, true, false);
} else if (msg.type === 'CHAT_MESSAGES') {
for (let i = msg.messages.length - 1; i >= 0; i--) {
chat.addMessage(msg.messages[i], true, true);
}
if (!chat.gotInitalMessages) {
chat.scrollDown();
chat.gotInitalMessages = true;
chat.historyPointer = clientVars.chatHead - msg.messages.length;
}
// messages are loaded, so hide the loading-ball
$('#chatloadmessagesball').css('display', 'none');
// there are less than 100 messages or we reached the top
if (chat.historyPointer <= 0) {
$('#chatloadmessagesbutton').css('display', 'none');
} else {
// there are still more messages, re-show the load-button
$('#chatloadmessagesbutton').css('display', 'block');
}
}
// HACKISH: User messages do not have "payload" but "userInfo", so that all
// "handleClientMessage_USER_" hooks would work, populate payload
// FIXME: USER_* messages to have "payload" property instead of "userInfo",
// seems like a quite a big work
if (msg.type.indexOf('USER_') > -1) {
msg.payload = msg.userInfo;
}
// Similar for NEW_CHANGES
if (msg.type === 'NEW_CHANGES') msg.payload = msg;
hooks.callAll(`handleClientMessage_${msg.type}`, {payload: msg.payload});
};
const updateUserInfo = (userInfo) => {
userInfo.userId = userId;
userSet[userId] = userInfo;
tellAceActiveAuthorInfo(userInfo);
if (!getSocket()) return;
sendMessage(
{
type: 'USERINFO_UPDATE',
userInfo,
});
};
const tellAceActiveAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
};
const tellAceAuthorInfo = (userId, colorId, inactive) => {
if (typeof colorId === 'number') {
colorId = clientVars.colorPalette[colorId];
}
const cssColor = colorId;
if (inactive) {
editor.setAuthorInfo(userId, {
bgcolor: cssColor,
fade: 0.5,
});
} else {
editor.setAuthorInfo(userId, {
bgcolor: cssColor,
});
}
};
const fadeAceAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
};
const getConnectedUsers = () => valuesArray(userSet);
const tellAceAboutHistoricalAuthors = (hadata) => {
for (const [author, data] of Object.entries(hadata)) {
if (!userSet[author]) {
tellAceAuthorInfo(author, data.colorId, true);
}
}
};
const setChannelState = (newChannelState, moreInfo) => {
if (newChannelState !== channelState) {
channelState = newChannelState;
callbacks.onChannelStateChange(channelState, moreInfo);
}
};
const valuesArray = (obj) => {
const array = [];
$.each(obj, (k, v) => {
array.push(v);
});
return array;
};
// We need to present a working interface even before the socket
// is connected for the first time.
let deferredActions = [];
const defer = (func, tag) => function (...args) {
const action = () => {
func.call(this, ...args);
};
action.tag = tag;
if (channelState === 'CONNECTING') {
deferredActions.push(action);
} else {
action();
}
};
const doDeferredActions = (tag) => {
const newArray = [];
for (let i = 0; i < deferredActions.length; i++) {
const a = deferredActions[i];
if ((!tag) || (tag === a.tag)) {
a();
} else {
newArray.push(a);
}
}
deferredActions = newArray;
};
const sendClientMessage = (msg) => {
sendMessage(
{
type: 'CLIENT_MESSAGE',
payload: msg,
});
};
const getCurrentRevisionNumber = () => rev;
const getMissedChanges = () => {
const obj = {};
obj.userInfo = userSet[userId];
obj.baseRev = rev;
if (committing && stateMessage) {
obj.committedChangeset = stateMessage.changeset;
obj.committedChangesetAPool = stateMessage.apool;
editor.applyPreparedChangesetToBase();
}
const userChangesData = editor.prepareUserChangeset();
if (userChangesData.changeset) {
obj.furtherChangeset = userChangesData.changeset;
obj.furtherChangesetAPool = userChangesData.apool;
}
return obj;
};
const setStateIdle = () => {
committing = false;
callbacks.onInternalAction('newlyIdle');
schedulePerhapsCallIdleFuncs();
};
const setIsPendingRevision = (value) => {
isPendingRevision = value;
};
const idleFuncs = [];
const callWhenNotCommitting = (func) => {
idleFuncs.push(func);
schedulePerhapsCallIdleFuncs();
};
const schedulePerhapsCallIdleFuncs = () => {
setTimeout(() => {
if (!committing) {
while (idleFuncs.length > 0) {
const f = idleFuncs.shift();
f();
}
}
}, 0);
};
const self = {
setOnUserJoin: (cb) => {
callbacks.onUserJoin = cb;
},
setOnUserLeave: (cb) => {
callbacks.onUserLeave = cb;
},
setOnUpdateUserInfo: (cb) => {
callbacks.onUpdateUserInfo = cb;
},
setOnChannelStateChange: (cb) => {
callbacks.onChannelStateChange = cb;
},
setOnClientMessage: (cb) => {
callbacks.onClientMessage = cb;
},
setOnInternalAction: (cb) => {
callbacks.onInternalAction = cb;
},
setOnConnectionTrouble: (cb) => {
callbacks.onConnectionTrouble = cb;
},
updateUserInfo: defer(updateUserInfo),
handleMessageFromServer,
getConnectedUsers,
sendClientMessage,
sendMessage,
getCurrentRevisionNumber,
getMissedChanges,
callWhenNotCommitting,
addHistoricalAuthors: tellAceAboutHistoricalAuthors,
setChannelState,
setStateIdle,
setIsPendingRevision,
set commitDelay(ms) { commitDelay = ms; },
get commitDelay() { return commitDelay; },
};
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
tellAceActiveAuthorInfo(initialUserInfo);
editor.setProperty('userAuthor', userId);
editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
editor.setUserChangeNotificationCallback(handleUserChanges);
setUpSocket();
return self;
};
exports.getCollabClient = getCollabClient;

View file

@ -0,0 +1,524 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
import {Ace2Editor} from "./ace";
import {ClientAcceptCommitMessage, ClientNewChanges, ClientSendMessages, ClientSendUserInfoUpdate, ClientUserChangesMessage, ClientVarData, ClientVarMessage, HistoricalAuthorData, ServerVar, UserInfo} from "./types/SocketIOMessage";
import {Pad} from "./pad";
import AttributePool from "./AttributePool";
import {MapArrayType} from "../../node/types/MapType";
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks');
const browser = require('./vendors/browser');
// Dependency fill on init. This exists for `pad.socket` only.
// TODO: bind directly to the socket.
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
ACE's ready callback does not need to have fired yet.
"serverVars" are from calling doc.getCollabClientVars() on the server. */
export class CollabClient {
private editor: Ace2Editor;
private serverVars: ServerVar;
private initialUserInfo: any;
private pad: Pad;
private userSet = new Map<string, UserInfo> // userId -> userInfo
private channelState: string;
private initialStartConnectTime: number;
private commitDelay: number;
private committing: boolean;
private rev: number;
private userId: string
// We need to present a working interface even before the socket
// is connected for the first time.
private deferredActions:any[] = [];
private stateMessage?: ClientUserChangesMessage;
private lastCommitTime: number;
private isPendingRevision: boolean;
private idleFuncs: Function[] = [];
constructor(ace2editor: Ace2Editor, serverVars: ServerVar, initialUserInfo: UserInfo, options: {
colorPalette: MapArrayType<number>
}, pad: Pad) {
this.serverVars = serverVars
this.initialUserInfo = initialUserInfo
this.pad = pad // Inject pad to avoid a circular dependency.
this.editor = ace2editor;
this.rev = serverVars.rev;
this.committing = false;
this.channelState = 'CONNECTING';
this.lastCommitTime = 0;
this.initialStartConnectTime = 0;
this.commitDelay = 500;
this.userId = initialUserInfo.userId;
// var socket;
this.userSet.set(this.userId,initialUserInfo);
this.isPendingRevision = false;
if (browser.firefox) {
// Prevent "escape" from taking effect and canceling a comet connection;
// doesn't work if focus is on an iframe.
$(window).on('keydown', (evt) => {
if (evt.which === 27) {
evt.preventDefault();
}
});
}
this.tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
this.tellAceActiveAuthorInfo(initialUserInfo);
// @ts-ignore
this.editor.setProperty('userAuthor', this.userId);
// @ts-ignore
this.editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
// @ts-ignore
this.editor.setUserChangeNotificationCallback(this.handleUserChanges);
this.setUpSocket();
}
callbacks = {
onUserJoin: (userInfo: UserInfo) => {},
onUserLeave: (userInfo: UserInfo) => {},
onUpdateUserInfo: (userInfo: UserInfo) => {},
onChannelStateChange: (newChannelState: string, moreInfo?: string) => {},
onClientMessage: (clientmessage: ClientSendMessages) => {},
onInternalAction: (res: string) => {},
onConnectionTrouble: (res?: string) => {},
onServerMessage: () => {},
}
handleUserChanges = () => {
if (this.editor.getInInternationalComposition()) {
// handleUserChanges() will be called again once composition ends so there's no need to set up
// a future call before returning.
return;
}
const now = Date.now();
if ((!this.pad.socket) || this.channelState === 'CONNECTING') {
if (this.channelState === 'CONNECTING' && (now - this.initialStartConnectTime) > 20000) {
this.setChannelState('DISCONNECTED', 'initsocketfail');
} else {
// check again in a bit
setTimeout(this.handleUserChanges, 1000);
}
return;
}
if (this.committing) {
if (now - this.lastCommitTime > 20000) {
// a commit is taking too long
this.setChannelState('DISCONNECTED', 'slowcommit');
} else if (now - this.lastCommitTime > 5000) {
this.callbacks.onConnectionTrouble('SLOW');
} else {
// run again in a few seconds, to detect a disconnect
setTimeout(this.handleUserChanges, 3000);
}
return;
}
const earliestCommit = this.lastCommitTime + this.commitDelay;
if (now < earliestCommit) {
setTimeout(this.handleUserChanges, earliestCommit - now);
return;
}
let sentMessage = false;
// Check if there are any pending revisions to be received from server.
// Allow only if there are no pending revisions to be received from server
if (!this.isPendingRevision) {
const userChangesData = this.editor.prepareUserChangeset();
if (userChangesData.changeset) {
this.lastCommitTime = now;
this.committing = true;
this.stateMessage = {
type: 'USER_CHANGES',
baseRev: this.rev,
changeset: userChangesData.changeset,
apool: userChangesData.apool,
} satisfies ClientUserChangesMessage;
this.sendMessage(this.stateMessage);
sentMessage = true;
this.callbacks.onInternalAction('commitPerformed');
}
} else {
// run again in a few seconds, to check if there was a reconnection attempt
setTimeout(this.handleUserChanges, 3000);
}
if (sentMessage) {
// run again in a few seconds, to detect a disconnect
setTimeout(this.handleUserChanges, 3000);
}
}
acceptCommit = () => {
// @ts-ignore
this.editor.applyPreparedChangesetToBase();
this.setStateIdle();
try {
this.callbacks.onInternalAction('commitAcceptedByServer');
this.callbacks.onConnectionTrouble('OK');
} catch (err) { /* intentionally ignored */ }
this.handleUserChanges();
}
setUpSocket = () => {
this.setChannelState('CONNECTED');
this.doDeferredActions();
this.initialStartConnectTime = Date.now();
}
sendMessage = (msg: ClientSendMessages) => {
this.pad.socket!.emit('message',
{
type: 'COLLABROOM',
component: 'pad',
data: msg,
});
}
serverMessageTaskQueue = new class {
private _promiseChain: Promise<any>
constructor() {
this._promiseChain = Promise.resolve();
}
async enqueue(fn: (val: any)=>void) {
const taskPromise = this._promiseChain.then(fn);
// Use .catch() to prevent rejections from halting the queue.
this._promiseChain = taskPromise.catch(() => {});
// Do NOT do `return await this._promiseChain;` because the caller would not see an error if
// fn() throws/rejects (due to the .catch() added above).
return await taskPromise;
}
}()
handleMessageFromServer = (evt: ClientVarMessage) => {
if (!this.pad.socket()) return;
if (!("data" in evt)) return;
const wrapper = evt;
if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM') return;
const msg = wrapper.data;
if (msg.type === 'NEW_CHANGES') {
this.serverMessageTaskQueue.enqueue(async () => {
// Avoid updating the DOM while the user is composing a character. Notes about this `await`:
// * `await null;` is equivalent to `await Promise.resolve(null);`, so if the user is not
// currently composing a character then execution will continue without error.
// * We assume that it is not possible for a new 'compositionstart' event to fire after
// the `await` but before the next line of code after the `await` (or, if it is
// possible, that the chances are so small or the consequences so minor that it's not
// worth addressing).
await this.editor.getInInternationalComposition();
const {newRev, changeset, author = '', apool} = msg;
if (newRev !== (this.rev + 1)) {
window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${this.rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_newchanges");
return;
}
this.rev = newRev;
// @ts-ignore
this.editor.applyChangesToBase(changeset, author, apool);
});
} else if (msg.type === 'ACCEPT_COMMIT') {
this.serverMessageTaskQueue.enqueue(() => {
const {newRev} = msg as ClientAcceptCommitMessage;
// newRev will equal rev if the changeset has no net effect (identity changeset, removing
// and re-adding the same characters with the same attributes, or retransmission of an
// already applied changeset).
if (![this.rev, this.rev + 1].includes(newRev)) {
window.console.warn(`bad message revision on ACCEPT_COMMIT: ${newRev} not ${this.rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_acceptcommit");
return;
}
this.rev = newRev;
this.acceptCommit();
});
} else if (msg.type === 'CLIENT_RECONNECT') {
// Server sends a CLIENT_RECONNECT message when there is a client reconnect.
// Server also returns all pending revisions along with this CLIENT_RECONNECT message
this.serverMessageTaskQueue.enqueue(() => {
if (msg.noChanges) {
// If no revisions are pending, just make everything normal
this.setIsPendingRevision(false);
return;
}
const {headRev, newRev, changeset, author = '', apool} = msg;
if (newRev !== (this.rev + 1)) {
window.console.warn(`bad message revision on CLIENT_RECONNECT: ${newRev} not ${this.rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_acceptcommit");
return;
}
this.rev = newRev;
if (author === this.pad.getUserId()) {
this.acceptCommit();
} else {
// @ts-ignore
this.editor.applyChangesToBase(changeset, author, apool);
}
if (newRev === headRev) {
// Once we have applied all pending revisions, make everything normal
this.setIsPendingRevision(false);
}
});
} else if (msg.type === 'USER_NEWINFO') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (this.userSet.has(id)) {
this.userSet.set(id,userInfo);
this.callbacks.onUpdateUserInfo(userInfo);
} else {
this.userSet.set(id,userInfo);
this.callbacks.onUserJoin(userInfo);
}
this.tellAceActiveAuthorInfo(userInfo);
} else if (msg.type === 'USER_LEAVE') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (this.userSet.has(id)) {
this.userSet.delete(userInfo.userId);
this.fadeAceAuthorInfo(userInfo);
this.callbacks.onUserLeave(userInfo);
}
} else if (msg.type === 'CLIENT_MESSAGE') {
this.callbacks.onClientMessage(msg.payload);
} else if (msg.type === 'CHAT_MESSAGE') {
chat.addMessage(msg.message, true, false);
} else if (msg.type === 'CHAT_MESSAGES') {
for (let i = msg.messages.length - 1; i >= 0; i--) {
chat.addMessage(msg.messages[i], true, true);
}
if (!chat.gotInitalMessages) {
chat.scrollDown();
chat.gotInitalMessages = true;
chat.historyPointer = window.clientVars.chatHead - msg.messages.length;
}
// messages are loaded, so hide the loading-ball
$('#chatloadmessagesball').css('display', 'none');
// there are less than 100 messages or we reached the top
if (chat.historyPointer <= 0) {
$('#chatloadmessagesbutton').css('display', 'none');
} else {
// there are still more messages, re-show the load-button
$('#chatloadmessagesbutton').css('display', 'block');
}
}
// HACKISH: User messages do not have "payload" but "userInfo", so that all
// "handleClientMessage_USER_" hooks would work, populate payload
// FIXME: USER_* messages to have "payload" property instead of "userInfo",
// seems like a quite a big work
if (msg.type.indexOf('USER_') > -1) {
// @ts-ignore
msg.payload = msg.userInfo;
}
// Similar for NEW_CHANGES
if (msg.type === 'NEW_CHANGES') {
msg.payload = msg;
}
// @ts-ignore
hooks.callAll(`handleClientMessage_${msg.type}`, {payload: msg.payload});
}
updateUserInfo = (userInfo: UserInfo) => {
userInfo.userId = this.userId;
this.userSet.set(this.userId, userInfo);
this.tellAceActiveAuthorInfo(userInfo);
if (!this.pad.socket()) return;
this.sendMessage(
{
type: 'USERINFO_UPDATE',
userInfo,
});
};
tellAceActiveAuthorInfo = (userInfo: UserInfo) => {
this.tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
}
tellAceAuthorInfo = (userId: string, colorId: number|object, inactive?: boolean) => {
if (typeof colorId === 'number') {
colorId = window.clientVars.colorPalette[colorId];
}
const cssColor = colorId;
if (inactive) {
// @ts-ignore
this.editor.setAuthorInfo(userId, {
bgcolor: cssColor,
fade: 0.5,
});
} else {
// @ts-ignore
this.editor.setAuthorInfo(userId, {
bgcolor: cssColor,
});
}
}
fadeAceAuthorInfo = (userInfo: UserInfo) => {
this.tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
}
getConnectedUsers = () => this.valuesArray(this.userSet);
tellAceAboutHistoricalAuthors = (hadata: HistoricalAuthorData) => {
for (const [author, data] of Object.entries(hadata)) {
if (!this.userSet.has(author)) {
this.tellAceAuthorInfo(author, data.colorId, true);
}
}
}
setChannelState = (newChannelState: string, moreInfo?: string) => {
if (newChannelState !== this.channelState) {
this.channelState = newChannelState;
this.callbacks.onChannelStateChange(this.channelState, moreInfo);
}
}
valuesArray = (obj: Map<string, UserInfo>) => {
const array: UserInfo[] = [];
for (let entry of obj.values()) {
array.push(entry)
}
return array;
};
defer = (func: Function, tag?: string) => (...args:any[])=> {
const action = () => {
func.call(this, ...args);
};
action.tag = tag;
if (this.channelState === 'CONNECTING') {
this.deferredActions.push(action);
} else {
action();
}
}
doDeferredActions = (tag?: string) => {
const newArray = [];
for (let i = 0; i < this.deferredActions.length; i++) {
const a = this.deferredActions[i];
if ((!tag) || (tag === a.tag)) {
a();
} else {
newArray.push(a);
}
}
this.deferredActions = newArray;
}
sendClientMessage = (msg: ClientSendMessages) => {
this.sendMessage(
{
type: 'CLIENT_MESSAGE',
payload: msg,
});
}
getCurrentRevisionNumber = () => this.rev
getMissedChanges = () => {
const obj:{
userInfo?: UserInfo,
baseRev?: number,
committedChangeset?: string,
committedChangesetAPool?: AttributePool,
furtherChangeset?: string,
furtherChangesetAPool?: AttributePool
} = {};
obj.userInfo = this.userSet.get(this.userId);
obj.baseRev = this.rev;
if (this.committing && this.stateMessage) {
obj.committedChangeset = this.stateMessage.changeset;
obj.committedChangesetAPool = this.stateMessage.apool;
// @ts-ignore
this.editor.applyPreparedChangesetToBase();
}
const userChangesData = this.editor.prepareUserChangeset();
if (userChangesData.changeset) {
obj.furtherChangeset = userChangesData.changeset;
obj.furtherChangesetAPool = userChangesData.apool;
}
return obj;
}
setStateIdle = () => {
this.committing = false;
this.callbacks.onInternalAction('newlyIdle');
this.schedulePerhapsCallIdleFuncs();
}
setIsPendingRevision = (value: boolean) => {
this.isPendingRevision = value;
}
callWhenNotCommitting = (func: Function) => {
this.idleFuncs.push(func);
this.schedulePerhapsCallIdleFuncs();
}
schedulePerhapsCallIdleFuncs = () => {
setTimeout(() => {
if (!this.committing) {
while (this.idleFuncs.length > 0) {
const f = this.idleFuncs.shift()!;
f();
}
}
}, 0);
}
setOnUserJoin= (cb: (userInfo: UserInfo)=>void) => {
this.callbacks.onUserJoin = cb;
}
setOnUserLeave= (cb: (userInfo: UserInfo) => void) => {
this.callbacks.onUserLeave = cb;
}
setOnUpdateUserInfo= (cb: (userInfo: UserInfo) => void) => {
this.callbacks.onUpdateUserInfo = cb;
}
setOnChannelStateChange = (cb: (newChannelState: string, moreInfo?: string) => void) => {
this.callbacks.onChannelStateChange = cb;
}
setOnClientMessage = (cb: (clientmessage: ClientSendMessages) => void) => {
this.callbacks.onClientMessage = cb;
}
setOnInternalAction = (cb: (res: string) => void) => {
this.callbacks.onInternalAction = cb;
}
setOnConnectionTrouble = (cb: (res?: string) => void) => {
this.callbacks.onConnectionTrouble = cb;
}
pupdateUserInfo = this.defer(this.updateUserInfo)
addHistoricalAuthors= this.tellAceAboutHistoricalAuthors
setCommitDelay = (ms: number) => {
this.commitDelay = ms
}
}
export default CollabClient

View file

@ -1,121 +0,0 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const colorutils = {};
// Check that a given value is a css hex color value, e.g.
// "#ffffff" or "#fff"
colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor);
// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0]
colorutils.css2triple = (cssColor) => {
const sixHex = colorutils.css2sixhex(cssColor);
const hexToFloat = (hh) => Number(`0x${hh}`) / 255;
return [
hexToFloat(sixHex.substr(0, 2)),
hexToFloat(sixHex.substr(2, 2)),
hexToFloat(sixHex.substr(4, 2)),
];
};
// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff"
colorutils.css2sixhex = (cssColor) => {
let h = /[0-9a-fA-F]+/.exec(cssColor)[0];
if (h.length !== 6) {
const a = h.charAt(0);
const b = h.charAt(1);
const c = h.charAt(2);
h = a + a + b + b + c + c;
}
return h;
};
// [1.0, 1.0, 1.0] -> "#ffffff"
colorutils.triple2css = (triple) => {
const floatToHex = (n) => {
const n2 = colorutils.clamp(Math.round(n * 255), 0, 255);
return (`0${n2.toString(16)}`).slice(-2);
};
return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`;
};
colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v);
colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c);
colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c);
colorutils.colorMin = (c) => colorutils.min3(c[0], c[1], c[2]);
colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]);
colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1);
colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1);
colorutils.scaleColor = (c, bot, top) => [
colorutils.scale(c[0], bot, top),
colorutils.scale(c[1], bot, top),
colorutils.scale(c[2], bot, top),
];
colorutils.unscaleColor = (c, bot, top) => [
colorutils.unscale(c[0], bot, top),
colorutils.unscale(c[1], bot, top),
colorutils.unscale(c[2], bot, top),
];
// rule of thumb for RGB brightness; 1.0 is white
colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11;
colorutils.saturate = (c) => {
const min = colorutils.colorMin(c);
const max = colorutils.colorMax(c);
if (max - min <= 0) return [1.0, 1.0, 1.0];
return colorutils.unscaleColor(c, min, max);
};
colorutils.blend = (c1, c2, t) => [
colorutils.scale(t, c1[0], c2[0]),
colorutils.scale(t, c1[1], c2[1]),
colorutils.scale(t, c1[2], c2[2]),
];
colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]];
colorutils.complementary = (c) => {
const inv = colorutils.invert(c);
return [
(inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30),
(inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59),
(inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11),
];
};
colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => {
const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff';
const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222';
return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black;
};
exports.colorutils = colorutils;

113
src/static/js/colorutils.ts Normal file
View file

@ -0,0 +1,113 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
type ColorTriplet = [number, number, number]
export class Colorutils {
// Check that a given value is a css hex color value, e.g.
// "#ffffff" or "#fff"
isCssHex = (cssColor: string) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor)
// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0]
css2triple = (cssColor: string): ColorTriplet => {
const sixHex = this.css2sixhex(cssColor);
const hexToFloat = (hh: string) => Number(`0x${hh}`) / 255;
return [
hexToFloat(sixHex.substring(0, 2)),
hexToFloat(sixHex.substring(2, 2)),
hexToFloat(sixHex.substring(4, 2)),
];
}
// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff"
css2sixhex = (cssColor: string) => {
let h = /[0-9a-fA-F]+/.exec(cssColor)![0];
if (h.length !== 6) {
const a = h.charAt(0);
const b = h.charAt(1);
const c = h.charAt(2);
h = a + a + b + b + c + c;
}
return h;
}
// [1.0, 1.0, 1.0] -> "#ffffff"
triple2css = (triple: number[]) => {
const floatToHex = (n:number) => {
const n2 = this.clamp(Math.round(n * 255), 0, 255);
return (`0${n2.toString(16)}`).slice(-2);
};
return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`;
}
clamp = (v: number, bot: number, top: number) => v < bot ? bot : (v > top ? top : v)
min3 = (a: number, b: number, c: number) => (a < b) ? (a < c ? a : c) : (b < c ? b : c)
max3 = (a: number, b: number, c: number) => (a > b) ? (a > c ? a : c) : (b > c ? b : c)
colorMin = (c: ColorTriplet) => this.min3(c[0], c[1], c[2])
colorMax = (c: ColorTriplet) => this.max3(c[0], c[1], c[2])
scale = (v: number, bot: number, top: number) => this.clamp(bot + v * (top - bot), 0, 1)
unscale = (v: number, bot: number, top: number) => this.clamp((v - bot) / (top - bot), 0, 1);
scaleColor = (c: ColorTriplet, bot: number, top: number) => [
this.scale(c[0], bot, top),
this.scale(c[1], bot, top),
this.scale(c[2], bot, top),
]
unscaleColor = (c: ColorTriplet, bot: number, top: number) => [
this.unscale(c[0], bot, top),
this.unscale(c[1], bot, top),
this.unscale(c[2], bot, top),
]
// rule of thumb for RGB brightness; 1.0 is white
luminosity = (c: ColorTriplet) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11
saturate = (c: ColorTriplet) => {
const min = this.colorMin(c);
const max = this.colorMax(c);
if (max - min <= 0) return [1.0, 1.0, 1.0];
return this.unscaleColor(c, min, max);
}
blend = (c1: ColorTriplet, c2: ColorTriplet, t: number) => [
this.scale(t, c1[0], c2[0]),
this.scale(t, c1[1], c2[1]),
this.scale(t, c1[2], c2[2]),
]
invert = (c: ColorTriplet) => [1 - c[0], 1 - c[1], 1 - c[2]]
complementary = (c: ColorTriplet) => {
const inv = this.invert(c);
return [
(inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30),
(inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59),
(inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11),
];
}
textColorFromBackgroundColor = (bgcolor: string, skinName: string) => {
const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff';
const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222';
return this.luminosity(this.css2triple(bgcolor)) < 0.5 ? white : black;
}
}
const colorutils = new Colorutils();
export default colorutils

View file

@ -8,6 +8,8 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector // THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); // %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins"); // %APPJET%: import("etherpad.admin.plugins");
import AttributePool from "./AttributePool";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -27,12 +29,22 @@
const _MAX_LIST_LEVEL = 16; const _MAX_LIST_LEVEL = 16;
import AttributeMap from './AttributeMap' import AttributeMap from './AttributeMap'
const UNorm = require('unorm'); import UNorm from 'unorm'
import {MapArrayType} from "../../node/types/MapType";
import {SmartOpAssembler} from "./SmartOpAssembler";
import {Attribute} from "./types/Attribute";
import {Browser} from "@playwright/test";
import {BrowserDetector} from "./vendors/browser";
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s); type Tag = {
const tagName = (n) => n.tagName && n.tagName.toLowerCase(); tagName: string
}
const sanitizeUnicode = (s: string) => UNorm.nfc(s);
const tagName = (n: Element) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin // supportedElems are Supported natively within Etherpad and don't require a plugin
const supportedElems = new Set([ const supportedElems = new Set([
'author', 'author',
@ -56,131 +68,178 @@ const supportedElems = new Set([
'ul', 'ul',
]); ]);
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => { type ContentElem = Element & {
const _blockElems = { name?: string
div: 1, }
p: 1,
pre: 1,
li: 1,
};
hooks.callAll('ccRegisterBlockElements').forEach((element) => { class Lines {
_blockElems[element] = 1; private textArray: string[] = [];
supportedElems.add(element); private attribsArray: string[] = [];
}); private attribsBuilder:SmartOpAssembler|null = null;
private op = new Changeset.Op('+');
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
const textify = (str) => sanitizeUnicode( length= () => this.textArray.length
str.replace(/(\n | \n)/g, ' ') atColumnZero = () => this.textArray[this.textArray.length - 1] === ''
.replace(/[\n\r ]/g, ' ') startNew= () => {
.replace(/\xa0/g, ' ') this.textArray.push('');
.replace(/\t/g, ' ')); this.flush(true);
this.attribsBuilder = new SmartOpAssembler();
}
textOfLine= (i: number) => this.textArray[i]
appendText= (txt: string, attrString = '') => {
this.textArray[this.textArray.length - 1] += txt;
this.op.attribs = attrString;
this.op.chars = txt.length;
this.attribsBuilder!.append(this.op);
}
textLines= () => this.textArray.slice()
attribLines= () => this.attribsArray
// call flush only when you're done
flush= (_withNewline?: boolean) => {
if (this.attribsBuilder) {
this.attribsArray.push(this.attribsBuilder.toString());
this.attribsBuilder = null;
}
}
}
const getAssoc = (node, name) => node[`_magicdom_${name}`]; type ContentCollectorState = {
author?:string
authorLevel?: number
listNesting?: number
lineAttributes: {
list?: string,
img?: string
start?: number
},
start?: number
flags: MapArrayType<number>,
attribs: MapArrayType<number>
attribString: string
localAttribs: string[]|null,
unsupportedElements: Set<string>
}
const lines = (() => { type ContentCollectorPoint = {
const textArray = []; index: number;
const attribsArray = []; node: Node
let attribsBuilder = null; }
const op = new Changeset.Op('+');
const self = {
length: () => textArray.length,
atColumnZero: () => textArray[textArray.length - 1] === '',
startNew: () => {
textArray.push('');
self.flush(true);
attribsBuilder = Changeset.smartOpAssembler();
},
textOfLine: (i) => textArray[i],
appendText: (txt, attrString = '') => {
textArray[textArray.length - 1] += txt;
op.attribs = attrString;
op.chars = txt.length;
attribsBuilder.append(op);
},
textLines: () => textArray.slice(),
attribLines: () => attribsArray,
// call flush only when you're done
flush: (withNewline) => {
if (attribsBuilder) {
attribsArray.push(attribsBuilder.toString());
attribsBuilder = null;
}
},
};
self.startNew();
return self;
})();
const cc = {};
const _ensureColumnZero = (state) => { type ContentCollectorSel = {
if (!lines.atColumnZero()) { startPoint: ContentCollectorPoint
cc.startNewLine(state); endPoint: ContentCollectorPoint
}
class ContentCollector {
private blockElems: MapArrayType<number>;
private cc = {};
private selection?: ContentCollectorSel
private startPoint?: ContentCollectorPoint
private endPoint?: ContentCollectorPoint;
private selStart = [-1, -1];
private selEnd = [-1, -1];
private collectStyles: boolean;
private apool: AttributePool;
private className2Author: (c: string) => string;
private breakLine?: boolean
private abrowser?: null|BrowserDetector;
constructor(collectStyles: boolean, abrowser: null, apool: AttributePool, className2Author: (c: string)=>string) {
this.blockElems = {
div: 1,
p: 1,
pre: 1,
li: 1,
} }
}; this.abrowser = abrowser
let selection, startPoint, endPoint; this.collectStyles = collectStyles
let selStart = [-1, -1]; this.apool = apool
let selEnd = [-1, -1]; this.className2Author = className2Author
const _isEmpty = (node, state) => {
hooks.callAll('ccRegisterBlockElements').forEach((element: "div"|"p"|"pre"|"li") => {
this.blockElems[element] = 1;
supportedElems.add(element);
})
}
isBlockElement = (n: Element) => !!this.blockElems[tagName(n) || ''];
textify = (str: string) => sanitizeUnicode(
str.replace(/(\n | \n)/g, ' ')
.replace(/[\n\r ]/g, ' ')
.replace(/\xa0/g, ' ')
.replace(/\t/g, ' '))
getAssoc = (node: MapArrayType<string>, name: string) => node[`_magicdom_${name}`];
lines = (() => {
const line = new Lines()
line.startNew()
return line;
})();
private ensureColumnZero = (state: ContentCollectorState|null) => {
if (!this.lines.atColumnZero()) {
this.startNewLine(state);
}
}
private isEmpty = (node: Element, state?: ContentCollectorState) => {
// consider clean blank lines pasted in IE to be empty // consider clean blank lines pasted in IE to be empty
if (node.childNodes.length === 0) return true; if (node.childNodes.length === 0) return true;
if (node.childNodes.length === 1 && if (node.childNodes.length === 1 &&
getAssoc(node, 'shouldBeEmpty') && // @ts-ignore
node.innerHTML === '&nbsp;' && this.getAssoc(node, 'shouldBeEmpty') &&
!getAssoc(node, 'unpasted')) { node.innerHTML === '&nbsp;' &&
// @ts-ignore
!this.getAssoc(node, 'unpasted')) {
if (state) { if (state) {
const child = node.childNodes[0]; const child = node.childNodes[0];
_reachPoint(child, 0, state); this.reachPoint(child, 0, state);
_reachPoint(child, 1, state); this.reachPoint(child, 1, state);
} }
return true; return true;
} }
return false; return false;
}; }
pointHere = (charsAfter: number, state: ContentCollectorState) => {
const _pointHere = (charsAfter, state) => { const ln = this.lines.length() - 1;
const ln = lines.length() - 1; let chr = this.lines.textOfLine(ln).length;
let chr = lines.textOfLine(ln).length;
if (chr === 0 && Object.keys(state.lineAttributes).length !== 0) { if (chr === 0 && Object.keys(state.lineAttributes).length !== 0) {
chr += 1; // listMarker chr += 1; // listMarker
} }
chr += charsAfter; chr += charsAfter;
return [ln, chr]; return [ln, chr];
}; }
const _reachBlockPoint = (nd, idx, state) => { reachBlockPoint = (nd: ContentElem, idx: number, state: ContentCollectorState) => {
if (nd.nodeType !== nd.TEXT_NODE) _reachPoint(nd, idx, state); if (nd.nodeType !== nd.TEXT_NODE) this.reachPoint(nd, idx, state);
}; }
reachPoint = (nd: Node, idx: number, state: ContentCollectorState) => {
const _reachPoint = (nd, idx, state) => { if (this.startPoint && nd === this.startPoint.node && this.startPoint.index === idx) {
if (startPoint && nd === startPoint.node && startPoint.index === idx) { this.selStart = this.pointHere(0, state);
selStart = _pointHere(0, state);
} }
if (endPoint && nd === endPoint.node && endPoint.index === idx) { if (this.endPoint && nd === this.endPoint.node && this.endPoint.index === idx) {
selEnd = _pointHere(0, state); this.selEnd = this.pointHere(0, state);
} }
}; }
cc.incrementFlag = (state, flagName) => { incrementFlag = (state: ContentCollectorState, flagName: string) => {
state.flags[flagName] = (state.flags[flagName] || 0) + 1; state.flags[flagName] = (state.flags[flagName] || 0) + 1;
}; }
cc.decrementFlag = (state, flagName) => { decrementFlag = (state: ContentCollectorState, flagName: string) => {
state.flags[flagName]--; state.flags[flagName]--;
}; }
cc.incrementAttrib = (state, attribName) => { incrementAttrib = (state: ContentCollectorState, attribName: string) => {
if (!state.attribs[attribName]) { if (!state.attribs[attribName]) {
state.attribs[attribName] = 1; state.attribs[attribName] = 1;
} else { } else {
state.attribs[attribName]++; state.attribs[attribName]++;
} }
_recalcAttribString(state); this.recalcAttribString(state);
}; }
cc.decrementAttrib = (state, attribName) => { decrementAttrib = (state: ContentCollectorState, attribName: string) => {
state.attribs[attribName]--; state.attribs[attribName]--;
_recalcAttribString(state); this.recalcAttribString(state);
}; }
private enterList = (state: ContentCollectorState, listType?: string) => {
const _enterList = (state, listType) => {
if (!listType) return; if (!listType) return;
const oldListType = state.lineAttributes.list; const oldListType = state.lineAttributes.list;
if (listType !== 'none') { if (listType !== 'none') {
@ -196,13 +255,13 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} else { } else {
state.lineAttributes.list = listType; state.lineAttributes.list = listType;
} }
_recalcAttribString(state); this.recalcAttribString(state);
return oldListType; return oldListType;
}; }
const _exitList = (state, oldListType) => { private exitList = (state: ContentCollectorState, oldListType: string) => {
if (state.lineAttributes.list) { if (state.lineAttributes.list) {
state.listNesting--; state.listNesting!--;
} }
if (oldListType && oldListType !== 'none') { if (oldListType && oldListType !== 'none') {
state.lineAttributes.list = oldListType; state.lineAttributes.list = oldListType;
@ -210,25 +269,22 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
delete state.lineAttributes.list; delete state.lineAttributes.list;
delete state.lineAttributes.start; delete state.lineAttributes.start;
} }
_recalcAttribString(state); this.recalcAttribString(state);
}; }
private enterAuthor = (state: ContentCollectorState, author: string) => {
const _enterAuthor = (state, author) => {
const oldAuthor = state.author; const oldAuthor = state.author;
state.authorLevel = (state.authorLevel || 0) + 1; state.authorLevel = (state.authorLevel || 0) + 1;
state.author = author; state.author = author;
_recalcAttribString(state); this.recalcAttribString(state);
return oldAuthor; return oldAuthor;
}; }
private exitAuthor = (state: ContentCollectorState, oldAuthor: string) => {
const _exitAuthor = (state, oldAuthor) => { state.authorLevel!--;
state.authorLevel--;
state.author = oldAuthor; state.author = oldAuthor;
_recalcAttribString(state); this.recalcAttribString(state);
}; }
private recalcAttribString = (state: ContentCollectorState) => {
const _recalcAttribString = (state) => { const attribs = new AttributeMap(this.apool);
const attribs = new AttributeMap(apool);
for (const [a, count] of Object.entries(state.attribs)) { for (const [a, count] of Object.entries(state.attribs)) {
if (!count) continue; if (!count) continue;
// The following splitting of the attribute name is a workaround // The following splitting of the attribute name is a workaround
@ -253,49 +309,50 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
attribs.set(a, 'true'); attribs.set(a, 'true');
} }
} }
if (state.authorLevel > 0) { if (state.authorLevel! > 0) {
if (apool.putAttrib(['author', state.author], true) >= 0) { if (this.apool!.putAttrib(['author', state.author!], true) >= 0) {
// require that author already be in pool // require that author already be in pool
// (don't add authors from other documents, etc.) // (don't add authors from other documents, etc.)
if (state.author) attribs.set('author', state.author); if (state.author) attribs.set('author', state.author);
} }
} }
state.attribString = attribs.toString(); state.attribString = attribs.toString();
}; }
private produceLineAttributesMarker = (state: ContentCollectorState) => {
const _produceLineAttributesMarker = (state) => {
// TODO: This has to go to AttributeManager. // TODO: This has to go to AttributeManager.
const attribs = new AttributeMap(apool) const attribsF = Object.entries(state.lineAttributes).map(([k, v]) => [k, v || '']) as Attribute[]
.set('lmkr', '1') const attribs = new AttributeMap(this.apool)
.set('insertorder', 'first') .set('lmkr', '1')
// TODO: Converting all falsy values in state.lineAttributes into removals is awkward. .set('insertorder', 'first')
// Better would be to never add 0, false, null, or undefined to state.lineAttributes in the // TODO: Converting all falsy values in state.lineAttributes into removals is awkward.
// first place (I'm looking at you, state.lineAttributes.start). // Better would be to never add 0, false, null, or undefined to state.lineAttributes in the
.update(Object.entries(state.lineAttributes).map(([k, v]) => [k, v || '']), true); // first place (I'm looking at you, state.lineAttributes.start).
lines.appendText('*', attribs.toString()); .update(attribsF, true);
}; this.lines.appendText('*', attribs.toString());
cc.startNewLine = (state) => { }
startNewLine = (state: ContentCollectorState|null) => {
if (state) { if (state) {
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0; const atBeginningOfLine = this.lines.textOfLine(this.lines.length() - 1).length === 0;
if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) { if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) {
_produceLineAttributesMarker(state); this.produceLineAttributesMarker(state);
} }
} }
lines.startNew(); this.lines.startNew();
}; }
cc.notifySelection = (sel) => { notifySelection = (sel: ContentCollectorSel) => {
if (sel) { if (sel) {
selection = sel; this.selection = sel;
startPoint = selection.startPoint; this.startPoint = this.selection.startPoint;
endPoint = selection.endPoint; this.endPoint = this.selection.endPoint;
} }
}; }
cc.doAttrib = (state, na) => { doAttrib = (state: ContentCollectorState, na: string) => {
state.localAttribs = (state.localAttribs || []); state.localAttribs = (state.localAttribs || []);
state.localAttribs.push(na); state.localAttribs.push(na);
cc.incrementAttrib(state, na); this.incrementAttrib(state, na);
}; }
cc.collectContent = function (node, state) {
collectContent = (node: ContentElem, state: ContentCollectorState)=> {
let unsupportedElements = null; let unsupportedElements = null;
if (!state) { if (!state) {
state = { state = {
@ -318,33 +375,33 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
const localAttribs = state.localAttribs; const localAttribs = state.localAttribs;
state.localAttribs = null; state.localAttribs = null;
const isBlock = isBlockElement(node); const isBlock = this.isBlockElement(node);
if (!isBlock && node.name && (node.name !== 'body')) { if (!isBlock && node.name && (node.name !== 'body')) {
if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name); if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name);
} }
const isEmpty = _isEmpty(node, state); const isEmpty = this.isEmpty(node, state);
if (isBlock) _ensureColumnZero(state); if (isBlock) this.ensureColumnZero(state);
const startLine = lines.length() - 1; const startLine = this.lines.length() - 1;
_reachBlockPoint(node, 0, state); this.reachBlockPoint(node, 0, state);
if (node.nodeType === node.TEXT_NODE) { if (node.nodeType === node.TEXT_NODE) {
const tname = node.parentNode.getAttribute('name'); const tname = (node.parentNode as Element)!.getAttribute('name');
const context = {cc: this, state, tname, node, text: node.nodeValue}; const context = {cc: this, state, tname, node, text: node.nodeValue};
// Hook functions may either return a string (deprecated) or modify context.text. If any hook // Hook functions may either return a string (deprecated) or modify context.text. If any hook
// function modifies context.text then all returned strings are ignored. If no hook functions // function modifies context.text then all returned strings are ignored. If no hook functions
// modify context.text, the first hook function to return a string wins. // modify context.text, the first hook function to return a string wins.
const [hookTxt] = const [hookTxt] =
hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string'); hooks.callAll('collectContentLineText', context).filter((s: string|object) => typeof s === 'string');
let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text; let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text;
let rest = ''; let rest = '';
let x = 0; // offset into original text let x = 0; // offset into original text
if (txt.length === 0) { if (txt.length === 0) {
if (startPoint && node === startPoint.node) { if (this.startPoint && node === this.startPoint.node) {
selStart = _pointHere(0, state); this.selStart = this.pointHere(0, state);
} }
if (endPoint && node === endPoint.node) { if (this.endPoint && node === this.endPoint.node) {
selEnd = _pointHere(0, state); this.selEnd = this.pointHere(0, state);
} }
} }
while (txt.length > 0) { while (txt.length > 0) {
@ -356,11 +413,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
txt = firstLine; txt = firstLine;
} else { /* will only run this loop body once */ } else { /* will only run this loop body once */
} }
if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) { if (this.startPoint && node === this.startPoint.node && this.startPoint.index - x <= txt.length) {
selStart = _pointHere(startPoint.index - x, state); this.selStart = this.pointHere(this.startPoint.index - x, state);
} }
if (endPoint && node === endPoint.node && endPoint.index - x <= txt.length) { if (this.endPoint && node === this.endPoint.node && this.endPoint.index - x <= txt.length) {
selEnd = _pointHere(endPoint.index - x, state); this.selEnd = this.pointHere(this.endPoint.index - x, state);
} }
let txt2 = txt; let txt2 = txt;
if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt)) { if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt)) {
@ -370,27 +427,27 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// removing "\n" from pasted HTML will collapse words together. // removing "\n" from pasted HTML will collapse words together.
txt2 = ''; txt2 = '';
} }
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0; const atBeginningOfLine = this.lines.textOfLine(this.lines.length() - 1).length === 0;
if (atBeginningOfLine) { if (atBeginningOfLine) {
// newlines in the source mustn't become spaces at beginning of line box // newlines in the source mustn't become spaces at beginning of line box
txt2 = txt2.replace(/^\n*/, ''); txt2 = txt2.replace(/^\n*/, '');
} }
if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) { if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) {
_produceLineAttributesMarker(state); this.produceLineAttributesMarker(state);
} }
lines.appendText(textify(txt2), state.attribString); this.lines.appendText(this.textify(txt2), state.attribString);
x += consumed; x += consumed;
txt = rest; txt = rest;
if (txt.length > 0) { if (txt.length > 0) {
cc.startNewLine(state); this.startNewLine(state);
} }
} }
} else if (node.nodeType === node.ELEMENT_NODE) { } else if (node.nodeType === node.ELEMENT_NODE) {
const tname = tagName(node) || ''; const tname = tagName(node as Element) || '';
if (tname === 'img') { if (tname === 'img') {
hooks.callAll('collectContentImage', { hooks.callAll('collectContentImage', {
cc, cc: this,
state, state,
tname, tname,
styl: null, styl: null,
@ -414,18 +471,18 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cls: null, cls: null,
}); });
if (startNewLine) { if (startNewLine) {
cc.startNewLine(state); this.startNewLine(state);
} }
} else if (tname === 'script' || tname === 'style') { } else if (tname === 'script' || tname === 'style') {
// ignore // ignore
} else if (!isEmpty) { } else if (!isEmpty) {
let styl = node.getAttribute('style'); let styl = node.getAttribute('style');
let cls = node.getAttribute('class'); let cls = node.getAttribute('class');
let isPre = (tname === 'pre'); let isPre: boolean| RegExpExecArray|"" = (tname === 'pre');
if ((!isPre) && abrowser && abrowser.safari) { if ((!isPre) && this.abrowser && this.abrowser.safari) {
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl))!;
} }
if (isPre) cc.incrementFlag(state, 'preMode'); if (isPre) this.incrementFlag(state, 'preMode');
let oldListTypeOrNull = null; let oldListTypeOrNull = null;
let oldAuthorOrNull = null; let oldAuthorOrNull = null;
@ -438,33 +495,33 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// for now it shows how to fix the problem // for now it shows how to fix the problem
return; return;
} }
if (collectStyles) { if (this.collectStyles) {
hooks.callAll('collectContentPre', { hooks.callAll('collectContentPre', {
cc, cc: this,
state, state,
tname, tname,
styl, styl,
cls, cls,
}); });
if (tname === 'b' || if (tname === 'b' ||
(styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
tname === 'strong') { tname === 'strong') {
cc.doAttrib(state, 'bold'); this.doAttrib(state, 'bold');
} }
if (tname === 'i' || if (tname === 'i' ||
(styl && /\bfont-style:\s*italic\b/i.exec(styl)) || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
tname === 'em') { tname === 'em') {
cc.doAttrib(state, 'italic'); this.doAttrib(state, 'italic');
} }
if (tname === 'u' || if (tname === 'u' ||
(styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
tname === 'ins') { tname === 'ins') {
cc.doAttrib(state, 'underline'); this.doAttrib(state, 'underline');
} }
if (tname === 's' || if (tname === 's' ||
(styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
tname === 'del') { tname === 'del') {
cc.doAttrib(state, 'strikethrough'); this.doAttrib(state, 'strikethrough');
} }
if (tname === 'ul' || tname === 'ol') { if (tname === 'ul' || tname === 'ol') {
let type = node.getAttribute('class'); let type = node.getAttribute('class');
@ -473,8 +530,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// check if we find a better hint within the node's children // check if we find a better hint within the node's children
if (!rr && !type) { if (!rr && !type) {
for (const child of node.childNodes) { for (const child of node.childNodes) {
if (tagName(child) !== 'ul') continue; if (tagName(child as ContentElem) !== 'ul') continue;
type = child.getAttribute('class'); type = (child as ContentElem).getAttribute('class');
if (type) break; if (type) break;
} }
} }
@ -493,22 +550,24 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
} }
oldListTypeOrNull = (_enterList(state, type) || 'none'); oldListTypeOrNull = (this.enterList(state, type) || 'none');
} else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) { } else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
// This has undesirable behavior in Chrome but is right in other browsers. // This has undesirable behavior in Chrome but is right in other browsers.
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning // See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none'); if (!this.abrowser!.chrome) {
oldListTypeOrNull = (this.enterList(state, undefined) || 'none');
}
} else if (tname === 'li') { } else if (tname === 'li') {
state.lineAttributes.start = state.start || 0; state.lineAttributes.start = state.start || 0;
_recalcAttribString(state); this.recalcAttribString(state);
if (state.lineAttributes.list.indexOf('number') !== -1) { if (state.lineAttributes.list!.indexOf('number') !== -1) {
/* /*
Nested OLs are not --> <ol><li>1</li><ol>nested</ol></ol> Nested OLs are not --> <ol><li>1</li><ol>nested</ol></ol>
They are --> <ol><li>1</li><li><ol><li>nested</li></ol></li></ol> They are --> <ol><li>1</li><li><ol><li>nested</li></ol></li></ol>
Note how the <ol> item has to be inside a <li> Note how the <ol> item has to be inside a <li>
Because of this we don't increment the start number Because of this we don't increment the start number
*/ */
if (node.parentNode && tagName(node.parentNode) !== 'ol') { if (node.parentNode && tagName(node.parentNode as Element) !== 'ol') {
/* /*
TODO: start number has to increment based on indentLevel(numberX) TODO: start number has to increment based on indentLevel(numberX)
This means we have to build an object IE This means we have to build an object IE
@ -521,12 +580,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
with exports? We can.. But let's leave this comment in because it might be useful with exports? We can.. But let's leave this comment in because it might be useful
in the future.. in the future..
*/ */
state.start++; // not if it's parent is an OL or UL. state.start!++; // not if it's parent is an OL or UL.
} }
} }
// UL list items never modify the start value. // UL list items never modify the start value.
if (node.parentNode && tagName(node.parentNode) === 'ul') { if (node.parentNode && tagName(node.parentNode as Element) === 'ul') {
state.start++; state.start!++;
// TODO, this is hacky. // TODO, this is hacky.
// Because if the first item is an UL it will increment a list no? // Because if the first item is an UL it will increment a list no?
// A much more graceful way would be to say, ul increases if it's within an OL // A much more graceful way would be to say, ul increases if it's within an OL
@ -539,14 +598,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// delete state.listNesting; // delete state.listNesting;
// _recalcAttribString(state); // _recalcAttribString(state);
} }
if (className2Author && cls) { if (this.className2Author && cls) {
const classes = cls.match(/\S+/g); const classes = cls.match(/\S+/g);
if (classes && classes.length > 0) { if (classes && classes.length > 0) {
for (let i = 0; i < classes.length; i++) { for (let i = 0; i < classes.length; i++) {
const c = classes[i]; const c = classes[i];
const a = className2Author(c); const a = this.className2Author(c);
if (a) { if (a) {
oldAuthorOrNull = (_enterAuthor(state, a) || 'none'); oldAuthorOrNull = (this.enterAuthor(state, a) || 'none');
break; break;
} }
} }
@ -555,12 +614,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
for (const c of node.childNodes) { for (const c of node.childNodes) {
cc.collectContent(c, state); this.collectContent(c as ContentElem, state);
} }
if (collectStyles) { if (this.collectStyles) {
hooks.callAll('collectContentPost', { hooks.callAll('collectContentPost', {
cc, cc: this,
state, state,
tname, tname,
styl, styl,
@ -568,23 +627,23 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
}); });
} }
if (isPre) cc.decrementFlag(state, 'preMode'); if (isPre) this.decrementFlag(state, 'preMode');
if (state.localAttribs) { if (state.localAttribs) {
for (let i = 0; i < state.localAttribs.length; i++) { for (let i = 0; i < state.localAttribs.length; i++) {
cc.decrementAttrib(state, state.localAttribs[i]); this.decrementAttrib(state, state.localAttribs[i]);
} }
} }
if (oldListTypeOrNull) { if (oldListTypeOrNull) {
_exitList(state, oldListTypeOrNull); this.exitList(state, oldListTypeOrNull);
} }
if (oldAuthorOrNull) { if (oldAuthorOrNull) {
_exitAuthor(state, oldAuthorOrNull); this.exitAuthor(state, oldAuthorOrNull);
} }
} }
} }
_reachBlockPoint(node, 1, state); this.reachBlockPoint(node, 1, state);
if (isBlock) { if (isBlock) {
if (lines.length() - 1 === startLine) { if (this.lines.length() - 1 === startLine) {
// added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20 // added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20
// this does mean that images etc can't be pasted on lists but imho that's fine // this does mean that images etc can't be pasted on lists but imho that's fine
@ -592,48 +651,50 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// Export events don't have window available. // Export events don't have window available.
// commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412 // commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412
if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') { if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') {
cc.startNewLine(state); this.startNewLine(state);
} }
} else { } else {
_ensureColumnZero(state); this.ensureColumnZero(state);
} }
} }
state.localAttribs = localAttribs; state.localAttribs = localAttribs;
if (unsupportedElements && unsupportedElements.size) { if (unsupportedElements && unsupportedElements.size) {
console.warn('Ignoring unsupported elements (you might want to install a plugin): ' + console.warn('Ignoring unsupported elements (you might want to install a plugin): ' +
`${[...unsupportedElements].join(', ')}`); `${[...unsupportedElements].join(', ')}`);
} }
}; }
// can pass a falsy value for end of doc // can pass a falsy value for end of doc
cc.notifyNextNode = (node) => { notifyNextNode = (node: ContentElem) => {
// an "empty block" won't end a line; this addresses an issue in IE with // an "empty block" won't end a line; this addresses an issue in IE with
// typing into a blank line at the end of the document. typed text // typing into a blank line at the end of the document. typed text
// goes into the body, and the empty line div still looks clean. // goes into the body, and the empty line div still looks clean.
// it is incorporated as dirty by the rule that a dirty region has // it is incorporated as dirty by the rule that a dirty region has
// to end a line. // to end a line.
if ((!node) || (isBlockElement(node) && !_isEmpty(node))) { if ((!node) || (this.isBlockElement(node) && !this.isEmpty(node))) {
_ensureColumnZero(null); this.ensureColumnZero(null);
} }
}; }
// each returns [line, char] or [-1,-1] // each returns [line, char] or [-1,-1]
const getSelectionStart = () => selStart; getSelectionStart = () => this.selStart;
const getSelectionEnd = () => selEnd; getSelectionEnd = () => this.selEnd;
// returns array of strings for lines found, last entry will be "" if // returns array of strings for lines found, last entry will be "" if
// last line is complete (i.e. if a following span should be on a new line). // last line is complete (i.e. if a following span should be on a new line).
// can be called at any point // can be called at any point
cc.getLines = () => lines.textLines(); getLines = () => this.lines.textLines();
cc.finish = () => { finish = () => {
lines.flush(); this.lines.flush();
const lineAttribs = lines.attribLines(); const lineAttribs = this.lines.attribLines();
const lineStrings = cc.getLines(); const lineStrings = this.getLines();
lineStrings.length--; lineStrings.length--;
lineAttribs.length--; lineAttribs.length--;
const ss = getSelectionStart(); const ss = this.getSelectionStart();
const se = getSelectionEnd(); const se = this.getSelectionEnd();
const fixLongLines = () => { const fixLongLines = () => {
// design mode does not deal with with really long lines! // design mode does not deal with with really long lines!
@ -645,7 +706,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
let oldString = lineStrings[i]; let oldString = lineStrings[i];
let oldAttribString = lineAttribs[i]; let oldAttribString = lineAttribs[i];
if (oldString.length > lineLimit + buffer) { if (oldString.length > lineLimit + buffer) {
const newStrings = []; const newStrings: string[] = [];
const newAttribStrings = []; const newAttribStrings = [];
while (oldString.length > lineLimit) { while (oldString.length > lineLimit) {
// var semiloc = oldString.lastIndexOf(';', lineLimit-1); // var semiloc = oldString.lastIndexOf(';', lineLimit-1);
@ -661,7 +722,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
newAttribStrings.push(oldAttribString); newAttribStrings.push(oldAttribString);
} }
const fixLineNumber = (lineChar) => { const fixLineNumber = (lineChar: number[]) => {
if (lineChar[0] < 0) return; if (lineChar[0] < 0) return;
let n = lineChar[0]; let n = lineChar[0];
let c = lineChar[1]; let c = lineChar[1];
@ -701,11 +762,5 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
lines: lineStrings, lines: lineStrings,
lineAttribs, lineAttribs,
}; };
}; }
}
return cc;
};
exports.sanitizeUnicode = sanitizeUnicode;
exports.makeContentCollector = makeContentCollector;
exports.supportedElems = supportedElems;

View file

@ -1,279 +0,0 @@
'use strict';
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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: plugins
// requires: undefined
const Security = require('security');
const hooks = require('./pluginfw/hooks');
const _ = require('underscore');
import {lineAttributeMarker} from "./linestylefilter";
const noop = () => {};
const domline = {};
domline.addToLineClass = (lineClass, cls) => {
// an "empty span" at any point can be used to add classes to
// the line, using line:className. otherwise, we ignore
// the span.
cls.replace(/\S+/g, (c) => {
if (c.indexOf('line:') === 0) {
// add class to line
lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5);
}
});
return lineClass;
};
// if "document" is falsy we don't create a DOM node, just
// an object with innerHTML and className
domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
const result = {
node: null,
appendSpan: noop,
prepareForAdd: noop,
notifyAdded: noop,
clearSpans: noop,
finishUpdate: noop,
lineMarker: 0,
};
const document = optDocument;
if (document) {
result.node = document.createElement('div');
// JAWS and NVDA screen reader compatibility. Only needed if in a real browser.
result.node.setAttribute('aria-live', 'assertive');
} else {
result.node = {
innerHTML: '',
className: '',
};
}
let html = [];
let preHtml = '';
let postHtml = '';
let curHTML = null;
const processSpaces = (s) => domline.processSpaces(s, doesWrap);
const perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
let lineClass = 'ace-line';
result.appendSpan = (txt, cls) => {
let processedMarker = false;
// Handle lineAttributeMarker, if present
if (cls.indexOf(lineAttributeMarker) >= 0) {
let listType = /(?:^| )list:(\S+)/.exec(cls);
const start = /(?:^| )start:(\S+)/.exec(cls);
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
domline,
cls,
}), (modifier) => {
preHtml += modifier.preHtml;
postHtml += modifier.postHtml;
processedMarker |= modifier.processedMarker;
});
if (listType) {
listType = listType[1];
if (listType) {
if (listType.indexOf('number') < 0) {
preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
postHtml = `</li></ul>${postHtml}`;
} else {
if (start) { // is it a start of a list with more than one item in?
if (Number.parseInt(start[1]) === 1) { // if its the first one at this level?
// Add start class to DIV node
lineClass = `${lineClass} ` + `list-start-${listType}`;
}
preHtml +=
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
} else {
// Handles pasted contents into existing lists
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
}
postHtml += '</li></ol>';
}
}
processedMarker = true;
}
_.map(hooks.callAll('aceDomLineProcessLineAttributes', {
domline,
cls,
}), (modifier) => {
preHtml += modifier.preHtml;
postHtml += modifier.postHtml;
processedMarker |= modifier.processedMarker;
});
if (processedMarker) {
result.lineMarker += txt.length;
return; // don't append any text
}
}
let href = null;
let simpleTags = null;
if (cls.indexOf('url') >= 0) {
cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => {
href = url;
return `${space}url`;
});
}
if (cls.indexOf('tag') >= 0) {
cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => {
if (!simpleTags) simpleTags = [];
simpleTags.push(tag.toLowerCase());
return space + tag;
});
}
let extraOpenTags = '';
let extraCloseTags = '';
_.map(hooks.callAll('aceCreateDomLine', {
domline,
cls,
}), (modifier) => {
cls = modifier.cls;
extraOpenTags += modifier.extraOpenTags;
extraCloseTags = modifier.extraCloseTags + extraCloseTags;
});
if ((!txt) && cls) {
lineClass = domline.addToLineClass(lineClass, cls);
} else if (txt) {
if (href) {
const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
// if the url doesn't include a protocol prefix, assume http
if (!~href.indexOf('://') && !urn_schemes.test(href)) {
href = `http://${href}`;
}
// Using rel="noreferrer" stops leaking the URL/location of the pad when
// clicking links in the document.
// Not all browsers understand this attribute, but it's part of the HTML5 standard.
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
// Additionally, we do rel="noopener" to ensure a higher level of referrer security.
// https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
// https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636
const escapedHref = Security.escapeHTMLAttribute(href);
extraOpenTags = `${extraOpenTags}<a href="${escapedHref}" rel="noreferrer noopener">`;
extraCloseTags = `</a>${extraCloseTags}`;
}
if (simpleTags) {
simpleTags.sort();
extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`;
simpleTags.reverse();
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
}
html.push(
'<span class="', Security.escapeHTMLAttribute(cls || ''),
'">',
extraOpenTags,
perTextNodeProcess(Security.escapeHTML(txt)),
extraCloseTags,
'</span>');
}
};
result.clearSpans = () => {
html = [];
lineClass = 'ace-line';
result.lineMarker = 0;
};
const writeHTML = () => {
let newHTML = perHtmlLineProcess(html.join(''));
if (!newHTML) {
if ((!document) || (!optBrowser)) {
newHTML += '&nbsp;';
} else {
newHTML += '<br/>';
}
}
if (nonEmpty) {
newHTML = (preHtml || '') + newHTML + (postHtml || '');
}
html = preHtml = postHtml = ''; // free memory
if (newHTML !== curHTML) {
curHTML = newHTML;
result.node.innerHTML = curHTML;
}
if (lineClass != null) result.node.className = lineClass;
hooks.callAll('acePostWriteDomLineHTML', {
node: result.node,
});
};
result.prepareForAdd = writeHTML;
result.finishUpdate = writeHTML;
return result;
};
domline.processSpaces = (s, doesWrap) => {
if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut
return s.replace(/ /g, '&nbsp;');
}
const parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, (m) => {
parts.push(m);
});
if (doesWrap) {
let endOfLine = true;
let beforeSpace = false;
// last space in a run is normal, others are nbsp,
// end of line is nbsp
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
if (p === ' ') {
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
} else if (p.charAt(0) !== '<') {
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === ' ') {
parts[i] = '&nbsp;';
break;
} else if (p.charAt(0) !== '<') {
break;
}
}
} else {
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === ' ') {
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
};
exports.domline = domline;

299
src/static/js/domline.ts Normal file
View file

@ -0,0 +1,299 @@
'use strict';
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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: plugins
// requires: undefined
const Security = require('security');
const hooks = require('./pluginfw/hooks');
const _ = require('underscore');
import {lineAttributeMarker} from "./linestylefilter";
const noop = () => {};
class Domline {
private node?: HTMLElement| {
innerHTML: '',
className: '',
}
html:string[] = [];
preHtml = '';
postHtml = '';
curHTML: string|null = null;
private lineMarker: number
private readonly doesWrap: boolean;
private optBrowser: string | undefined;
private optDocument: Document | undefined;
private lineClass = 'ace-line';
private nonEmpty: boolean;
constructor(nonEmpty: boolean, doesWrap: boolean, optBrowser?: string, optDocument?: Document) {
this.lineMarker = 0
this.doesWrap = doesWrap
this.nonEmpty = nonEmpty
this.optBrowser = optBrowser
this.optDocument = optDocument
}
addToLineClass = (lineClass: string, cls: string) => {
// an "empty span" at any point can be used to add classes to
// the line, using line:className. otherwise, we ignore
// the span.
cls.replace(/\S+/g, (c) => {
if (c.indexOf('line:') === 0) {
// add class to line
lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5);
return lineClass
}
return c
});
return lineClass;
}
ProcessSpaces = (s: string) => this.processSpaces(s, this.doesWrap);
perTextNodeProcess = (s: string):string=>{
if (this.doesWrap){
return _.identity()
} else {
return this.processSpaces(s)
}
}
perHtmlLineProcess = (s:string)=>{
if (this.doesWrap) {
return this.processSpaces(s)
} else {
return _.identity()
}
}
appendSpan = (txt: string, cls: string) => {
let processedMarker = false;
// Handle lineAttributeMarker, if present
if (cls.indexOf(lineAttributeMarker) >= 0) {
let listType = /(?:^| )list:(\S+)/.exec(cls);
const start = /(?:^| )start:(\S+)/.exec(cls);
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
domline: this,
cls,
}), (modifier: { preHtml: any; postHtml: any; processedMarker: boolean; }) => {
this.preHtml += modifier.preHtml;
this.postHtml += modifier.postHtml;
processedMarker ||= modifier.processedMarker;
});
if (listType) {
let listTypeExtracted = listType[1];
if (listTypeExtracted) {
if (listTypeExtracted.indexOf('number') < 0) {
this.preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listTypeExtracted)}"><li>`;
this.postHtml = `</li></ul>${this.postHtml}`;
} else {
if (start) { // is it a start of a list with more than one item in?
if (Number.parseInt(start[1]) === 1) { // if its the first one at this level?
// Add start class to DIV node
this.lineClass = `${this.lineClass} ` + `list-start-${listTypeExtracted}`;
}
this.preHtml +=
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listTypeExtracted)}"><li>`;
} else {
// Handles pasted contents into existing lists
this.preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listTypeExtracted)}"><li>`;
}
this.postHtml += '</li></ol>';
}
}
processedMarker = true;
}
_.map(hooks.callAll('aceDomLineProcessLineAttributes', {
domline: this,
cls,
}), (modifier: { preHtml: string; postHtml: string; processedMarker: boolean; }) => {
this.preHtml += modifier.preHtml;
this.postHtml += modifier.postHtml;
processedMarker ||= modifier.processedMarker;
});
if (processedMarker) {
this.lineMarker += txt.length;
return; // don't append any text
}
}
let href: null|string = null;
let simpleTags: null|string[] = null;
if (cls.indexOf('url') >= 0) {
cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url: string) => {
href = url;
return `${space}url`;
});
}
if (cls.indexOf('tag') >= 0) {
cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => {
if (!simpleTags) simpleTags = [];
simpleTags.push(tag.toLowerCase());
return space + tag;
});
}
let extraOpenTags = '';
let extraCloseTags = '';
_.map(hooks.callAll('aceCreateDomLine', {
domline: this,
cls,
}), (modifier: { cls: string; extraOpenTags: string; extraCloseTags: string; }) => {
cls = modifier.cls;
extraOpenTags += modifier.extraOpenTags;
extraCloseTags = modifier.extraCloseTags + extraCloseTags;
});
if ((!txt) && cls) {
this.lineClass = this.addToLineClass(this.lineClass, cls);
} else if (txt) {
if (href) {
const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
// if the url doesn't include a protocol prefix, assume http
// @ts-ignore
if (!~href.indexOf('://') && !urn_schemes.test(href)) {
href = `http://${href}`;
}
// Using rel="noreferrer" stops leaking the URL/location of the pad when
// clicking links in the document.
// Not all browsers understand this attribute, but it's part of the HTML5 standard.
// https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
// Additionally, we do rel="noopener" to ensure a higher level of referrer security.
// https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
// https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636
const escapedHref = Security.escapeHTMLAttribute(href);
extraOpenTags = `${extraOpenTags}<a href="${escapedHref}" rel="noreferrer noopener">`;
extraCloseTags = `</a>${extraCloseTags}`;
}
if (simpleTags) {
// @ts-ignore
simpleTags.sort();
// @ts-ignore
extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`;
// @ts-ignore
simpleTags.reverse();
// @ts-ignore
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
}
this.html.push(
'<span class="', Security.escapeHTMLAttribute(cls || ''),
'">',
extraOpenTags,
this.perTextNodeProcess(Security.escapeHTML(txt)),
extraCloseTags,
'</span>');
}
}
writeHTML = () => {
let newHTML = this.perHtmlLineProcess(this.html.join(''));
if (!newHTML) {
if ((!document) || (!this.optBrowser)) {
newHTML += '&nbsp;';
} else {
newHTML += '<br/>';
}
}
if (this.nonEmpty) {
newHTML = (this.preHtml || '') + newHTML + (this.postHtml || '');
}
this.html! = []
this.preHtml = this.postHtml = ''; // free memory
if (newHTML !== this.curHTML) {
this.curHTML = newHTML;
this.node!.innerHTML! = this.curHTML as string;
}
if (this.lineClass != null) this.node!.className = this.lineClass;
hooks.callAll('acePostWriteDomLineHTML', {
node: this.node,
});
};
clearSpans = () => {
this.html = [];
this.lineClass = 'ace-line';
this.lineMarker = 0;
}
prepareForAdd = this.writeHTML
finishUpdate = this.writeHTML
private processSpaces = (s: string, doesWrap?: boolean) => {
if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut
return s.replace(/ /g, '&nbsp;');
}
const parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, (m) => {
parts.push(m);
return m
});
if (doesWrap) {
let endOfLine = true;
let beforeSpace = false;
// last space in a run is normal, others are nbsp,
// end of line is nbsp
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
if (p === ' ') {
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
} else if (p.charAt(0) !== '<') {
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === ' ') {
parts[i] = '&nbsp;';
break;
} else if (p.charAt(0) !== '<') {
break;
}
}
} else {
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === ' ') {
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
}
}
// if "document" is falsy we don't create a DOM node, just
// an object with innerHTML and className
export default Domline

View file

@ -24,7 +24,7 @@ import {Socket} from "socket.io";
* limitations under the License. * limitations under the License.
*/ */
let socket: null | Socket; let socket: null | any;
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
@ -37,12 +37,12 @@ import html10n from './vendors/html10n'
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;
const chat = require('./chat').chat; const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient; import Collab_client, {CollabClient} from './collab_client'
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
import padcookie from "./pad_cookie"; import padcookie from "./pad_cookie";
const padeditbar = require('./pad_editbar').padeditbar; const padeditbar = require('./pad_editbar').padeditbar;
const padeditor = require('./pad_editor').padeditor; import {padEditor as padeditor} from './pad_editor'
const padimpexp = require('./pad_impexp').padimpexp; const padimpexp = require('./pad_impexp').padimpexp;
const padmodals = require('./pad_modals').padmodals; const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs'); const padsavedrevs = require('./pad_savedrevs');
@ -52,8 +52,9 @@ import {padUtils as padutils} from "./pad_utils";
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString; const randomString = require('./pad_utils').randomString;
import connect from './socketio' import connect from './socketio'
import {SocketClientReadyMessage} from "./types/SocketIOMessage"; import {ClientSendMessages, ClientVarData, ClientVarMessage, HistoricalAuthorData, PadOption, SocketClientReadyMessage, SocketIOMessage, UserInfo} from "./types/SocketIOMessage";
import {MapArrayType} from "../../node/types/MapType"; import {MapArrayType} from "../../node/types/MapType";
import {ChangeSetLoader} from "./timeslider";
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
@ -84,7 +85,7 @@ const getParameters = [
checkVal: null, checkVal: null,
callback: (val: any) => { callback: (val: any) => {
if (val === 'false') { if (val === 'false') {
settings.hideChat = true; pad.settings.hideChat = true;
chat.hide(); chat.hide();
$('#chaticon').hide(); $('#chaticon').hide();
} }
@ -93,58 +94,59 @@ const getParameters = [
{ {
name: 'showLineNumbers', name: 'showLineNumbers',
checkVal: 'false', checkVal: 'false',
callback: (val) => { callback: (val: any) => {
settings.LineNumbersDisabled = true; pad.settings.LineNumbersDisabled = true;
}, },
}, },
{ {
name: 'useMonospaceFont', name: 'useMonospaceFont',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
settings.useMonospaceFontGlobal = true; pad.settings.useMonospaceFontGlobal = true;
}, },
}, },
{ {
name: 'userName', name: 'userName',
checkVal: null, checkVal: null,
callback: (val) => { callback: (val: string) => {
settings.globalUserName = val; pad.settings.globalUserName = val;
window.clientVars.userName = val; window.clientVars.userName = val;
}, },
}, },
{ {
name: 'userColor', name: 'userColor',
checkVal: null, checkVal: null,
callback: (val) => { callback: (val: number) => {
settings.globalUserColor = val; // @ts-ignore
pad.settings.globalUserColor = val;
window.clientVars.userColor = val; window.clientVars.userColor = val;
}, },
}, },
{ {
name: 'rtl', name: 'rtl',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
settings.rtlIsTrue = true; pad.settings.rtlIsTrue = true;
}, },
}, },
{ {
name: 'alwaysShowChat', name: 'alwaysShowChat',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
if (!settings.hideChat) chat.stickToScreen(); if (!pad.settings.hideChat) chat.stickToScreen();
}, },
}, },
{ {
name: 'chatAndUsers', name: 'chatAndUsers',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
chat.chatAndUsers(); chat.chatAndUsers();
}, },
}, },
{ {
name: 'lang', name: 'lang',
checkVal: null, checkVal: null,
callback: (val) => { callback: (val: any) => {
console.log('Val is', val) console.log('Val is', val)
html10n.localize([val, 'en']); html10n.localize([val, 'en']);
Cookies.set('language', val); Cookies.set('language', val);
@ -155,6 +157,7 @@ const getParameters = [
const getParams = () => { const getParams = () => {
// Tries server enforced options first.. // Tries server enforced options first..
for (const setting of getParameters) { for (const setting of getParameters) {
// @ts-ignore
let value = window.clientVars.padOptions[setting.name]; let value = window.clientVars.padOptions[setting.name];
if (value == null) continue; if (value == null) continue;
value = value.toString(); value = value.toString();
@ -213,7 +216,7 @@ const sendClientReady = (isReconnect: boolean) => {
// this is a reconnect, lets tell the server our revisionnumber // this is a reconnect, lets tell the server our revisionnumber
if (isReconnect) { if (isReconnect) {
msg.client_rev = this.collabClient!.getCurrentRevisionNumber(); msg.client_rev = pad.collabClient!.getCurrentRevisionNumber();
msg.reconnect = true; msg.reconnect = true;
} }
@ -257,7 +260,7 @@ const handshake = async () => {
} }
}; };
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason: string) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
console.log(`Socket disconnected: ${reason}`) console.log(`Socket disconnected: ${reason}`)
@ -266,15 +269,18 @@ const handshake = async () => {
}); });
socket.on('shout', (obj) => { socket.on('shout', (obj: ClientVarMessage) => {
if (obj.type === "COLLABROOM") { if (obj.type === "COLLABROOM") {
// @ts-ignore
let date = new Date(obj.data.payload.timestamp); let date = new Date(obj.data.payload.timestamp);
$.gritter.add({ window.$.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: 'Admin message', title: 'Admin message',
// (string | mandatory) the text inside the notification // (string | mandatory) the text inside the notification
// @ts-ignore
text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message, text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message,
// (bool | optional) if you want it to fade out on its own or just sit there // (bool | optional) if you want it to fade out on its own or just sit there
// @ts-ignore
sticky: obj.data.payload.message.sticky sticky: obj.data.payload.message.sticky
}); });
} }
@ -282,7 +288,7 @@ const handshake = async () => {
socket.io.on('reconnect_attempt', socketReconnecting); socket.io.on('reconnect_attempt', socketReconnecting);
socket.io.on('reconnect_failed', (error) => { socket.io.on('reconnect_failed', (error: string) => {
// pad.collabClient might be null if the hanshake failed (or it never got that far). // pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) { if (pad.collabClient != null) {
pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout'); pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout');
@ -292,7 +298,7 @@ const handshake = async () => {
}); });
socket.on('error', (error) => { socket.on('error', (error: string) => {
// pad.collabClient might be null if the error occurred before the hanshake completed. // pad.collabClient might be null if the error occurred before the hanshake completed.
if (pad.collabClient != null) { if (pad.collabClient != null) {
pad.collabClient.setStateIdle(); pad.collabClient.setStateIdle();
@ -303,9 +309,9 @@ const handshake = async () => {
// just annoys users and fills logs. // just annoys users and fills logs.
}); });
socket.on('message', (obj) => { socket.on('message', (obj: ClientVarMessage) => {
// the access was not granted, give the user a message // the access was not granted, give the user a message
if (obj.accessStatus) { if ("accessStatus" in obj) {
if (obj.accessStatus === 'deny') { if (obj.accessStatus === 'deny') {
$('#loading').hide(); $('#loading').hide();
$('#permissionDenied').show(); $('#permissionDenied').show();
@ -334,7 +340,7 @@ const handshake = async () => {
}) })
} }
} else if (obj.disconnect) { } else if ("disconnect" in obj && obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect); padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect(); socket.disconnect();
@ -350,9 +356,9 @@ const handshake = async () => {
}); });
await Promise.all([ await Promise.all([
new Promise((resolve) => { new Promise<void>((resolve) => {
const h = (obj) => { const h = (obj: ClientVarData) => {
if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; if ("accessStatus" in obj || obj.type !== 'CLIENT_VARS') return;
socket.off('message', h); socket.off('message', h);
resolve(); resolve();
}; };
@ -368,39 +374,45 @@ const handshake = async () => {
/** Defers message handling until setCollabClient() is called with a non-null value. */ /** Defers message handling until setCollabClient() is called with a non-null value. */
class MessageQueue { class MessageQueue {
private _q: ClientVarMessage[]
private _cc: Collab_client | null
constructor() { constructor() {
this._q = []; this._q = [];
this._cc = null; this._cc = null;
} }
setCollabClient(cc) { setCollabClient(cc: Collab_client) {
this._cc = cc; this._cc = cc;
this.enqueue(); // Flush. this.enqueue(); // Flush.
} }
enqueue(...msgs) { enqueue(...msgs: ClientVarMessage[]) {
if (this._cc == null) { if (this._cc == null) {
this._q.push(...msgs); this._q.push(...msgs);
} else { } else {
while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift()); while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift()!);
for (const msg of msgs) this._cc.handleMessageFromServer(msg); for (const msg of msgs) this._cc.handleMessageFromServer(msg);
} }
} }
} }
export class Pad { export class Pad {
private collabClient: null; public collabClient: null| CollabClient;
private myUserInfo: null | { private myUserInfo: null | UserInfo &{
userId: string, globalUserColor?: string| boolean
name: string, name?: string
ip: string, ip?: string
colorId: string, };
private diagnosticInfo: {
disconnectedMessage?: string
padId?: string
socket?: MapArrayType<string|number>,
collabDiagnosticInfo?: any
}; };
private diagnosticInfo: {};
private initTime: number; private initTime: number;
private clientTimeOffset: null | number; private clientTimeOffset: null | number;
private _messageQ: MessageQueue; _messageQ: MessageQueue;
private padOptions: MapArrayType<MapArrayType<string>>; private padOptions: PadOption;
settings: PadSettings = { settings: PadSettings = {
LineNumbersDisabled: false, LineNumbersDisabled: false,
noColors: false, noColors: false,
@ -409,6 +421,7 @@ export class Pad {
globalUserColor: false, globalUserColor: false,
rtlIsTrue: false, rtlIsTrue: false,
} }
socket: any;
constructor() { constructor() {
// don't access these directly from outside this file, except // don't access these directly from outside this file, except
@ -430,8 +443,8 @@ export class Pad {
getUserId = () => this.myUserInfo!.userId getUserId = () => this.myUserInfo!.userId
getUserName = () => this.myUserInfo!.name getUserName = () => this.myUserInfo!.name
userList = () => paduserlist.users() userList = () => paduserlist.users()
sendClientMessage = (msg: string) => { sendClientMessage = (msg: ClientSendMessages) => {
this.collabClient.sendClientMessage(msg); this.collabClient!.sendClientMessage(msg);
} }
init = () => { init = () => {
padutils.setupGlobalExceptionHandler(); padutils.setupGlobalExceptionHandler();
@ -472,7 +485,7 @@ export class Pad {
const postAceInit = () => { const postAceInit = () => {
padeditbar.init(); padeditbar.init();
setTimeout(() => { setTimeout(() => {
padeditor.ace.focus(); padeditor.ace!.focus();
}, 0); }, 0);
const optionsStickyChat = $('#options-stickychat'); const optionsStickyChat = $('#options-stickychat');
optionsStickyChat.on('click', () => { optionsStickyChat.on('click', () => {
@ -502,14 +515,14 @@ export class Pad {
$('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update'); $('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update');
// Prevent sticky chat or chat and users to be checked for mobiles // Prevent sticky chat or chat and users to be checked for mobiles
const checkChatAndUsersVisibility = (x) => { const checkChatAndUsersVisibility = (x: MediaQueryListEvent|MediaQueryList) => {
if (x.matches) { // If media query matches if (x.matches) { // If media query matches
$('#options-chatandusers:checked').trigger('click'); $('#options-chatandusers:checked').trigger('click');
$('#options-stickychat:checked').trigger('click'); $('#options-stickychat:checked').trigger('click');
} }
}; };
const mobileMatch = window.matchMedia('(max-width: 800px)'); const mobileMatch = window.matchMedia('(max-width: 800px)');
mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized mobileMatch.addListener((ev)=>checkChatAndUsersVisibility(ev)); // check if window resized
setTimeout(() => { setTimeout(() => {
checkChatAndUsersVisibility(mobileMatch); checkChatAndUsersVisibility(mobileMatch);
}, 0); // check now after load }, 0); // check now after load
@ -522,21 +535,23 @@ export class Pad {
// order of inits is important here: // order of inits is important here:
padimpexp.init(this); padimpexp.init(this);
padsavedrevs.init(this); padsavedrevs.init(this);
// @ts-ignore
padeditor.init(this.padOptions.view || {}, this).then(postAceInit); padeditor.init(this.padOptions.view || {}, this).then(postAceInit);
paduserlist.init(this.myUserInfo, this); paduserlist.init(this.myUserInfo, this);
padconnectionstatus.init(); padconnectionstatus.init();
padmodals.init(this); padmodals.init(this);
this.collabClient = getCollabClient( this.collabClient = new CollabClient(
padeditor.ace, window.clientVars.collab_client_vars, this.myUserInfo, padeditor.ace!, window.clientVars.collab_client_vars, this.myUserInfo!,
{colorPalette: this.getColorPalette()}, pad); {colorPalette: this.getColorPalette()}, pad);
this._messageQ.setCollabClient(this.collabClient); this._messageQ.setCollabClient(this.collabClient);
this.collabClient.setOnUserJoin(this.handleUserJoin); this.collabClient.setOnUserJoin(this.handleUserJoin);
this.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); this.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
this.collabClient.setOnUserLeave(pad.handleUserLeave); this.collabClient.setOnUserLeave(pad.handleUserLeave);
this.collabClient.setOnClientMessage(pad.handleClientMessage); this.collabClient.setOnClientMessage(pad.handleClientMessage!);
// @ts-ignore
this.collabClient.setOnChannelStateChange(pad.handleChannelStateChange); this.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
this.collabClient.setOnInternalAction(pad.handleCollabAction); this.collabClient.setOnInternalAction(pad.handleCollabAction!);
// load initial chat-messages // load initial chat-messages
if (window.clientVars.chatHead !== -1) { if (window.clientVars.chatHead !== -1) {
@ -550,52 +565,54 @@ export class Pad {
if (window.clientVars.readonly) { if (window.clientVars.readonly) {
chat.hide(); chat.hide();
// @ts-ignore
$('#myusernameedit').attr('disabled', true); $('#myusernameedit').attr('disabled', true);
// @ts-ignore
$('#chatinput').attr('disabled', true); $('#chatinput').attr('disabled', true);
$('#chaticon').hide(); $('#chaticon').hide();
$('#options-chatandusers').parent().hide(); $('#options-chatandusers').parent().hide();
$('#options-stickychat').parent().hide(); $('#options-stickychat').parent().hide();
} else if (!settings.hideChat) { } else if (!this.settings.hideChat) {
$('#chaticon').show(); $('#chaticon').show();
} }
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite'); $('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');
padeditor.ace.callWithAce((ace) => { padeditor.ace!.callWithAce((ace) => {
ace.ace_setEditable(!window.clientVars.readonly); ace.ace_setEditable(!window.clientVars.readonly);
}); });
// If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers
if (settings.LineNumbersDisabled === true) { if (this.settings.LineNumbersDisabled === true) {
this.changeViewOption('showLineNumbers', false); this.changeViewOption('showLineNumbers', false);
} }
// If the noColors value is set to true then we need to // If the noColors value is set to true then we need to
// hide the background colors on the ace spans // hide the background colors on the ace spans
if (settings.noColors === true) { if (this.settings.noColors === true) {
this.changeViewOption('noColors', true); this.changeViewOption('noColors', true);
} }
if (settings.rtlIsTrue === true) { if (this.settings.rtlIsTrue === true) {
this.changeViewOption('rtlIsTrue', true); this.changeViewOption('rtlIsTrue', true);
} }
// If the Monospacefont value is set to true then change it to monospace. // If the Monospacefont value is set to true then change it to monospace.
if (settings.useMonospaceFontGlobal === true) { if (this.settings.useMonospaceFontGlobal === true) {
this.changeViewOption('padFontFamily', 'RobotoMono'); this.changeViewOption('padFontFamily', 'RobotoMono');
} }
// if the globalUserName value is set we need to tell the server and // if the globalUserName value is set we need to tell the server and
// the client about the new authorname // the client about the new authorname
if (settings.globalUserName !== false) { if (this.settings.globalUserName !== false) {
this.notifyChangeName(settings.globalUserName); // Notifies the server this.notifyChangeName(this.settings.globalUserName as string); // Notifies the server
this.myUserInfo.name = settings.globalUserName; this.myUserInfo!.name = this.settings.globalUserName as string;
$('#myusernameedit').val(settings.globalUserName); // Updates the current users UI $('#myusernameedit').val(this.settings.globalUserName as string); // Updates the current users UI
} }
if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { if (this.settings.globalUserColor !== false && colorutils.isCssHex(this.settings.globalUserColor)) {
// Add a 'globalUserColor' property to myUserInfo, // Add a 'globalUserColor' property to myUserInfo,
// so collabClient knows we have a query parameter. // so collabClient knows we have a query parameter.
this.myUserInfo.globalUserColor = settings.globalUserColor; this.myUserInfo!.globalUserColor = this.settings.globalUserColor!;
this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId this.notifyChangeColor(this.settings.globalUserColor as unknown as number); // Updates this.myUserInfo.colorId
paduserlist.setMyUserInfo(this.myUserInfo); paduserlist.setMyUserInfo(this.myUserInfo);
} }
} }
@ -603,39 +620,39 @@ export class Pad {
dispose = () => { dispose = () => {
padeditor.dispose(); padeditor.dispose();
} }
notifyChangeName = (newName) => { notifyChangeName = (newName: string) => {
this.myUserInfo.name = newName; this.myUserInfo!.name = newName;
this.collabClient.updateUserInfo(this.myUserInfo); this.collabClient!.updateUserInfo(this.myUserInfo!);
} }
notifyChangeColor = (newColorId) => { notifyChangeColor = (newColorId: number) => {
this.myUserInfo.colorId = newColorId; this.myUserInfo!.colorId = newColorId;
this.collabClient.updateUserInfo(this.myUserInfo); this.collabClient!.updateUserInfo(this.myUserInfo!);
} }
changePadOption = (key: string, value: string) => { changePadOption = (key: string, value: string) => {
const options: MapArrayType<string> = {}; const options: any = {}; // PadOption
options[key] = value; options[key] = value;
this.handleOptionsChange(options); this.handleOptionsChange(options);
this.collabClient.sendClientMessage( this.collabClient!.sendClientMessage(
{ {
type: 'padoptions', type: 'padoptions',
options, options,
changedBy: this.myUserInfo.name || 'unnamed', changedBy: this.myUserInfo!.name || 'unnamed',
}) })
} }
changeViewOption = (key: string, value: string) => { changeViewOption = (key: string, value: any) => {
const options: MapArrayType<MapArrayType<any>> = const options: PadOption =
{ {
view: {} view: {}
, ,
} }
; ;
options.view[key] = value; options.view![key] = value;
this.handleOptionsChange(options); this.handleOptionsChange(options);
} }
handleOptionsChange = (opts: MapArrayType<MapArrayType<string>>) => { handleOptionsChange = (opts: PadOption) => {
// opts object is a full set of options or just // opts object is a full set of options or just
// some options to change // some options to change
if (opts.view) { if (opts.view) {
@ -643,7 +660,9 @@ export class Pad {
this.padOptions.view = {}; this.padOptions.view = {};
} }
for (const [k, v] of Object.entries(opts.view)) { for (const [k, v] of Object.entries(opts.view)) {
// @ts-ignore
this.padOptions.view[k] = v; this.padOptions.view[k] = v;
// @ts-ignore
padcookie.setPref(k, v); padcookie.setPref(k, v);
} }
padeditor.setViewOptions(this.padOptions.view); padeditor.setViewOptions(this.padOptions.view);
@ -652,28 +671,28 @@ export class Pad {
getPadOptions = () => this.padOptions getPadOptions = () => this.padOptions
suggestUserName = suggestUserName =
(userId: string, name: string) => { (userId: string, name: string) => {
this.collabClient.sendClientMessage( this.collabClient!.sendClientMessage(
{ {
type: 'suggestUserName', type: 'suggestUserName',
unnamedId: userId, unnamedId: userId,
newName: name, newName: name,
}); });
} }
handleUserJoin = (userInfo) => { handleUserJoin = (userInfo: UserInfo) => {
paduserlist.userJoinOrUpdate(userInfo); paduserlist.userJoinOrUpdate(userInfo);
} }
handleUserUpdate = (userInfo) => { handleUserUpdate = (userInfo: UserInfo) => {
paduserlist.userJoinOrUpdate(userInfo); paduserlist.userJoinOrUpdate(userInfo);
} }
handleUserLeave = handleUserLeave =
(userInfo) => { (userInfo: UserInfo) => {
paduserlist.userLeave(userInfo); paduserlist.userLeave(userInfo);
} }
// caller shouldn't mutate the object // caller shouldn't mutate the object
handleClientMessage = handleClientMessage =
(msg) => { (msg: ClientSendMessages) => {
if (msg.type === 'suggestUserName') { if (msg.type === 'suggestUserName') {
if (msg.unnamedId === pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) { if (msg.unnamedId === pad.myUserInfo!.userId && msg.newName && !pad.myUserInfo!.name) {
pad.notifyChangeName(msg.newName); pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo); paduserlist.setMyUserInfo(pad.myUserInfo);
} }
@ -689,7 +708,7 @@ export class Pad {
handleChannelStateChange handleChannelStateChange
= =
(newState, message) => { (newState: string, message: string) => {
const oldFullyConnected = !!padconnectionstatus.isFullyConnected(); const oldFullyConnected = !!padconnectionstatus.isFullyConnected();
const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting'); const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting');
if (newState === 'CONNECTED') { if (newState === 'CONNECTED') {
@ -709,10 +728,9 @@ export class Pad {
// we filter non objects from the socket object and put them in the diagnosticInfo // we filter non objects from the socket object and put them in the diagnosticInfo
// this ensures we have no cyclic data - this allows us to stringify the data // this ensures we have no cyclic data - this allows us to stringify the data
for (const [i, value] of Object.entries(socket.socket || {})) { for (const [i, value] of Object.entries(socket!.socket || {})) {
const type = typeof value;
if (type === 'string' || type === 'number') { if (typeof value === 'string' || typeof value === 'number') {
pad.diagnosticInfo.socket[i] = value; pad.diagnosticInfo.socket[i] = value;
} }
} }
@ -734,7 +752,7 @@ export class Pad {
} }
handleIsFullyConnected handleIsFullyConnected
= =
(isConnected, isInitialConnect) => { (isConnected: boolean, isInitialConnect: boolean) => {
pad.determineChatVisibility(isConnected && !isInitialConnect); pad.determineChatVisibility(isConnected && !isInitialConnect);
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect); pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect);
pad.determineAuthorshipColorsVisibility(); pad.determineAuthorshipColorsVisibility();
@ -744,7 +762,7 @@ export class Pad {
} }
determineChatVisibility determineChatVisibility
= =
(asNowConnectedFeedback) => { (asNowConnectedFeedback: boolean) => {
const chatVisCookie = padcookie.getPref('chatAlwaysVisible'); const chatVisCookie = padcookie.getPref('chatAlwaysVisible');
if (chatVisCookie) { // if the cookie is set for chat always visible if (chatVisCookie) { // if the cookie is set for chat always visible
chat.stickToScreen(true); // stick it to the screen chat.stickToScreen(true); // stick it to the screen
@ -755,7 +773,7 @@ export class Pad {
} }
determineChatAndUsersVisibility determineChatAndUsersVisibility
= =
(asNowConnectedFeedback) => { (asNowConnectedFeedback: boolean) => {
const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible'); const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible');
if (chatAUVisCookie) { // if the cookie is set for chat always visible if (chatAUVisCookie) { // if the cookie is set for chat always visible
chat.chatAndUsers(true); // stick it to the screen chat.chatAndUsers(true); // stick it to the screen
@ -777,7 +795,7 @@ export class Pad {
} }
handleCollabAction handleCollabAction
= =
(action) => { (action: string) => {
if (action === 'commitPerformed') { if (action === 'commitPerformed') {
padeditbar.setSyncStatus('syncing'); padeditbar.setSyncStatus('syncing');
} else if (action === 'newlyIdle') { } else if (action === 'newlyIdle') {
@ -806,26 +824,27 @@ export class Pad {
= =
() => { () => {
$('form#reconnectform input.padId').val(pad.getPadId()); $('form#reconnectform input.padId').val(pad.getPadId());
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo(); // @ts-ignore //FIxME What is that
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient!.getDiagnosticInfo();
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo)); $('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
$('form#reconnectform input.missedChanges') $('form#reconnectform input.missedChanges')
.val(JSON.stringify(pad.collabClient.getMissedChanges())); .val(JSON.stringify(pad.collabClient!.getMissedChanges()));
$('form#reconnectform').trigger('submit'); $('form#reconnectform').trigger('submit');
} }
callWhenNotCommitting callWhenNotCommitting
= =
(f) => { (f: Function) => {
pad.collabClient.callWhenNotCommitting(f); pad.collabClient!.callWhenNotCommitting(f);
} }
getCollabRevisionNumber getCollabRevisionNumber
= =
() => pad.collabClient.getCurrentRevisionNumber() () => pad.collabClient!.getCurrentRevisionNumber()
isFullyConnected isFullyConnected
= =
() => padconnectionstatus.isFullyConnected() () => padconnectionstatus.isFullyConnected()
addHistoricalAuthors addHistoricalAuthors
= =
(data) => { (data: HistoricalAuthorData) => {
if (!pad.collabClient) { if (!pad.collabClient) {
window.setTimeout(() => { window.setTimeout(() => {
pad.addHistoricalAuthors(data); pad.addHistoricalAuthors(data);
@ -849,9 +868,7 @@ export type PadSettings = {
export const pad = new Pad() export const pad = new Pad()
exports.baseURL = ''; exports.baseURL = '';
exports.randomString = randomString; exports.randomString = randomString;
exports.getParams = getParams; exports.getParams = getParams;
exports.pad = pad; exports.pad = pad;
exports.init = init;

View file

@ -1,8 +1,10 @@
'use strict'; 'use strict';
import html10n from './vendors/html10n'; import html10n from './vendors/html10n';
import {PadOption} from "./types/SocketIOMessage";
import {Pad} from "./pad";
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => { export const showCountDownTimerToReconnectOnModal = ($modal: JQuery<HTMLElement>, pad: Pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) { if (window.clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
createCountDownElementsIfNecessary($modal); createCountDownElementsIfNecessary($modal);
const timer = createTimerForModal($modal, pad); const timer = createTimerForModal($modal, pad);
@ -16,7 +18,7 @@ exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
} }
}; };
const createCountDownElementsIfNecessary = ($modal) => { const createCountDownElementsIfNecessary = ($modal: JQuery<HTMLElement>) => {
const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0; const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0;
if (elementsDoNotExist) { if (elementsDoNotExist) {
const $defaultMessage = $modal.find('#defaulttext'); const $defaultMessage = $modal.find('#defaulttext');
@ -48,16 +50,16 @@ const createCountDownElementsIfNecessary = ($modal) => {
} }
}; };
const localize = ($element) => { const localize = ($element: JQuery<HTMLElement>) => {
html10n.translateElement(html10n.translations, $element.get(0)); html10n.translateElement(html10n.translations, $element.get(0));
}; };
const createTimerForModal = ($modal, pad) => { const createTimerForModal = ($modal: JQuery<HTMLElement>, pad: Pad) => {
const timeUntilReconnection = const timeUntilReconnection =
clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry(); window.clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
const timer = new CountDownTimer(timeUntilReconnection); const timer = new CountDownTimer(timeUntilReconnection);
timer.onTick((minutes, seconds) => { timer.onTick((minutes: number, seconds: number) => {
updateCountDownTimerMessage($modal, minutes, seconds); updateCountDownTimerMessage($modal, minutes, seconds);
}).onExpire(() => { }).onExpire(() => {
const wasANetworkError = $modal.is('.disconnected'); const wasANetworkError = $modal.is('.disconnected');
@ -72,23 +74,23 @@ const createTimerForModal = ($modal, pad) => {
return timer; return timer;
}; };
const disableAutomaticReconnection = ($modal) => { const disableAutomaticReconnection = ($modal: JQuery<HTMLElement>) => {
toggleAutomaticReconnectionOption($modal, true); toggleAutomaticReconnectionOption($modal, true);
}; };
const enableAutomaticReconnection = ($modal) => { const enableAutomaticReconnection = ($modal: JQuery<HTMLElement>) => {
toggleAutomaticReconnectionOption($modal, false); toggleAutomaticReconnectionOption($modal, false);
}; };
const toggleAutomaticReconnectionOption = ($modal, disableAutomaticReconnect) => { const toggleAutomaticReconnectionOption = ($modal: JQuery<HTMLElement>, disableAutomaticReconnect: boolean) => {
$modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect); $modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect);
$modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect); $modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect);
}; };
const waitUntilClientCanConnectToServerAndThen = (callback, pad) => { const waitUntilClientCanConnectToServerAndThen = (callback: Function, pad: Pad) => {
whenConnectionIsRestablishedWithServer(callback, pad); whenConnectionIsRestablishedWithServer(callback, pad);
pad.socket.connect(); pad.socket.connect();
}; };
const whenConnectionIsRestablishedWithServer = (callback, pad) => { const whenConnectionIsRestablishedWithServer = (callback: Function, pad: Pad) => {
// only add listener for the first try, don't need to add another listener // only add listener for the first try, don't need to add another listener
// on every unsuccessful try // on every unsuccessful try
if (reconnectionTries.counter === 1) { if (reconnectionTries.counter === 1) {
@ -96,15 +98,15 @@ const whenConnectionIsRestablishedWithServer = (callback, pad) => {
} }
}; };
const forceReconnection = ($modal) => { const forceReconnection = ($modal: JQuery<HTMLElement>) => {
$modal.find('#forcereconnect').trigger('click'); $modal.find('#forcereconnect').trigger('click');
}; };
const updateCountDownTimerMessage = ($modal, minutes, seconds) => { const updateCountDownTimerMessage = ($modal: JQuery<HTMLElement>, minutes: number, seconds: number) => {
minutes = minutes < 10 ? `0${minutes}` : minutes; let minutesFormatted = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds; let secondsFormatted = seconds < 10 ? `0${seconds}` : seconds;
$modal.find('.timetoexpire').text(`${minutes}:${seconds}`); $modal.find('.timetoexpire').text(`${minutesFormatted}:${secondsFormatted}`);
}; };
// store number of tries to reconnect to server, in order to increase time to wait // store number of tries to reconnect to server, in order to increase time to wait
@ -125,71 +127,75 @@ const reconnectionTries = {
// duration: how many **seconds** until the timer ends // duration: how many **seconds** until the timer ends
// granularity (optional): how many **milliseconds** // granularity (optional): how many **milliseconds**
// between each 'tick' of timer. Default: 1000ms (1s) // between each 'tick' of timer. Default: 1000ms (1s)
const CountDownTimer = function (duration, granularity) {
this.duration = duration;
this.granularity = granularity || 1000;
this.running = false;
this.onTickCallbacks = []; class CountDownTimer {
this.onExpireCallbacks = []; private duration: number
}; private granularity: number
private running: boolean
private onTickCallbacks: Function[]
private onExpireCallbacks: Function[]
private timeoutId: any = 0
constructor(duration: number, granularity?: number) {
this.duration = duration;
this.granularity = granularity || 1000;
this.running = false;
CountDownTimer.prototype.start = function () { this.onTickCallbacks = [];
if (this.running) { this.onExpireCallbacks = [];
return;
} }
this.running = true; start = ()=> {
const start = Date.now(); if (this.running) {
const that = this; return;
let diff;
const timer = () => {
diff = that.duration - Math.floor((Date.now() - start) / 1000);
if (diff > 0) {
that.timeoutId = setTimeout(timer, that.granularity);
that.tick(diff);
} else {
that.running = false;
that.tick(0);
that.expire();
} }
}; this.running = true;
timer(); const start = Date.now();
}; const that = this;
let diff;
const timer = () => {
diff = that.duration - Math.floor((Date.now() - start) / 1000);
CountDownTimer.prototype.tick = function (diff) { if (diff > 0) {
const obj = CountDownTimer.parse(diff); that.timeoutId = setTimeout(timer, that.granularity);
this.onTickCallbacks.forEach(function (callback) { that.tick(diff);
callback.call(this, obj.minutes, obj.seconds); } else {
}, this); that.running = false;
}; that.tick(0);
CountDownTimer.prototype.expire = function () { that.expire();
this.onExpireCallbacks.forEach(function (callback) { }
callback.call(this); };
}, this); timer();
};
CountDownTimer.prototype.onTick = function (callback) {
if (typeof callback === 'function') {
this.onTickCallbacks.push(callback);
} }
return this; tick = (diff: number)=> {
}; const obj = this.parse(diff);
this.onTickCallbacks.forEach( (callback)=> {
CountDownTimer.prototype.onExpire = function (callback) { callback.call(this, obj.minutes, obj.seconds);
if (typeof callback === 'function') { }, this);
this.onExpireCallbacks.push(callback); }
expire = ()=> {
this.onExpireCallbacks.forEach( (callback)=> {
callback.call(this);
}, this);
}
onTick = (callback: Function)=> {
if (typeof callback === 'function') {
this.onTickCallbacks.push(callback);
}
return this;
} }
return this;
};
CountDownTimer.prototype.cancel = function () { onExpire = (callback: Function)=> {
this.running = false; if (typeof callback === 'function') {
clearTimeout(this.timeoutId); this.onExpireCallbacks.push(callback);
return this; }
}; return this;
}
CountDownTimer.parse = (seconds) => ({ cancel = () => {
minutes: (seconds / 60) | 0, this.running = false;
seconds: (seconds % 60) | 0, clearTimeout(this.timeoutId);
}); return this;
}
parse = (seconds: number) => ({
minutes: (seconds / 60) | 0,
seconds: (seconds % 60) | 0,
});
}

View file

@ -22,45 +22,51 @@
* limitations under the License. * limitations under the License.
*/ */
const padmodals = require('./pad_modals').padmodals; import {padModals} from "./pad_modals";
const padconnectionstatus = (() => { class PadConnectionStatus {
let status = { private status: {
what: string,
why?: string
} = {
what: 'connecting', what: 'connecting',
}; }
const self = { init = () => {
init: () => { $('button#forcereconnect').on('click', () => {
$('button#forcereconnect').on('click', () => { window.location.reload();
window.location.reload(); });
}); }
},
connected: () => {
status = {
what: 'connected',
};
padmodals.showModal('connected');
padmodals.hideOverlay();
},
reconnecting: () => {
status = {
what: 'reconnecting',
};
padmodals.showModal('reconnecting'); connected = () => {
padmodals.showOverlay(); this.status = {
}, what: 'connected',
disconnected: (msg) => { };
if (status.what === 'disconnected') return; padModals.showModal('connected');
padModals.hideOverlay();
}
reconnecting = () => {
this.status = {
what: 'reconnecting',
};
status = { padModals.showModal('reconnecting');
what: 'disconnected', padModals.showOverlay();
why: msg, }
}; disconnected
=
(msg: string) => {
if (this.status.what === 'disconnected') return;
// These message IDs correspond to localized strings that are presented to the user. If a new this.status =
// message ID is added here then a new div must be added to src/templates/pad.html and the {
// corresponding l10n IDs must be added to the language files in src/locales. what: 'disconnected',
why: msg,
}
// These message IDs correspond to localized strings that are presented to the user. If a new
// message ID is added here then a new div must be added to src/templates/pad.html and the
// corresponding l10n IDs must be added to the language files in src/locales.
const knownReasons = [ const knownReasons = [
'badChangeset', 'badChangeset',
'corruptPad', 'corruptPad',
@ -80,13 +86,17 @@ const padconnectionstatus = (() => {
k = 'disconnected'; k = 'disconnected';
} }
padmodals.showModal(k); padModals.showModal(k);
padmodals.showOverlay(); padModals.showOverlay();
}, }
isFullyConnected: () => status.what === 'connected', isFullyConnected
getStatus: () => status, =
}; () => this.status.what === 'connected'
return self; getStatus
})(); =
() => this.status
}
export const padconnectionstatus = new PadConnectionStatus()
exports.padconnectionstatus = padconnectionstatus; exports.padconnectionstatus = padconnectionstatus;

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {MapArrayType} from "../../node/types/MapType";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -26,13 +28,16 @@ const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
import {padUtils as padutils} from "./pad_utils"; import {padUtils as padutils} from "./pad_utils";
const padeditor = require('./pad_editor').padeditor; import {PadEditor, padEditor as padeditor} from "./pad_editor";
import {Ace2Editor} from "./ace";
import html10n from "./vendors/html10n";
const padsavedrevs = require('./pad_savedrevs'); const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore'); const _ = require('underscore');
require('./vendors/nice-select'); require('./vendors/nice-select');
class ToolbarItem { class ToolbarItem {
constructor(element) { private $el: JQuery<HTMLElement>;
constructor(element: JQuery<HTMLElement>) {
this.$el = element; this.$el = element;
} }
@ -46,8 +51,9 @@ class ToolbarItem {
} }
} }
setValue(val) { setValue(val: boolean) {
if (this.isSelect()) { if (this.isSelect()) {
// @ts-ignore
return this.$el.find('select').val(val); return this.$el.find('select').val(val);
} }
} }
@ -64,7 +70,7 @@ class ToolbarItem {
return this.getType() === 'button'; return this.getType() === 'button';
} }
bind(callback) { bind(callback: (cmd: string|undefined, tb: ToolbarItem)=>void) {
if (this.isButton()) { if (this.isButton()) {
this.$el.on('click', (event) => { this.$el.on('click', (event) => {
$(':focus').trigger('blur'); $(':focus').trigger('blur');
@ -79,52 +85,62 @@ class ToolbarItem {
} }
} }
const syncAnimation = (() => { class SyncAnimation {
const SYNCING = -100; static SYNCING = -100;
const DONE = 100; static DONE = 100;
let state = DONE; state = SyncAnimation.DONE;
const fps = 25; fps = 25;
const step = 1 / fps; step = 1 / this.fps;
const T_START = -0.5; static T_START = -0.5;
const T_FADE = 1.0; static T_FADE = 1.0;
const T_GONE = 1.5; static T_GONE = 1.5;
const animator = padutils.makeAnimationScheduler(() => { private animator: { scheduleAnimation: () => void };
if (state === SYNCING || state === DONE) { constructor() {
return false;
} else if (state >= T_GONE) {
state = DONE;
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'none');
return false;
} else if (state < 0) {
state += step;
if (state >= 0) {
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'block').css('opacity', 1);
}
return true;
} else {
state += step;
if (state >= T_FADE) {
$('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
}
return true;
}
}, step * 1000);
return {
syncing: () => {
state = SYNCING;
$('#syncstatussyncing').css('display', 'block');
$('#syncstatusdone').css('display', 'none');
},
done: () => {
state = T_START;
animator.scheduleAnimation();
},
};
})();
exports.padeditbar = new class { this.animator = padutils.makeAnimationScheduler(() => {
if (this.state === SyncAnimation.SYNCING || this.state === SyncAnimation.DONE) {
return false;
} else if (this.state >= SyncAnimation.T_GONE) {
this.state = SyncAnimation.DONE;
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'none');
return false;
} else if (this.state < 0) {
this.state += this.step;
if (this.state >= 0) {
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'block').css('opacity', 1);
}
return true;
} else {
this.state += this.step;
if (this.state >= SyncAnimation.T_FADE) {
$('#syncstatusdone').css('opacity', (SyncAnimation.T_GONE - this.state) / (SyncAnimation.T_GONE - SyncAnimation.T_FADE));
}
return true;
}
}, this.step * 1000);
}
syncing = () => {
this.state = SyncAnimation.SYNCING;
$('#syncstatussyncing').css('display', 'block');
$('#syncstatusdone').css('display', 'none');
}
done = () => {
this.state = SyncAnimation.T_START;
this.animator.scheduleAnimation();
}
}
const syncAnimation = new SyncAnimation()
type ToolbarCallback = (cmd: string, el: ToolbarItem)=>void
type ToolbarAceCallback = (cmd: string, ace: any, el: ToolbarItem)=>void
class Padeditbar {
private _editbarPosition: number;
private commands: MapArrayType<ToolbarCallback| ToolbarAceCallback>;
private dropdowns: any[];
constructor() { constructor() {
this._editbarPosition = 0; this._editbarPosition = 0;
this.commands = {}; this.commands = {};
@ -137,7 +153,7 @@ exports.padeditbar = new class {
$('#editbar [data-key]').each((i, elt) => { $('#editbar [data-key]').each((i, elt) => {
$(elt).off('click'); $(elt).off('click');
new ToolbarItem($(elt)).bind((command, item) => { new ToolbarItem($(elt)).bind((command, item) => {
this.triggerCommand(command, item); this.triggerCommand(command!, item);
}); });
}); });
@ -165,12 +181,13 @@ exports.padeditbar = new class {
* overflow:hidden on parent * overflow:hidden on parent
*/ */
if (!browser.safari) { if (!browser.safari) {
// @ts-ignore
$('select').niceSelect(); $('select').niceSelect();
} }
// When editor is scrolled, we add a class to style the editbar differently // When editor is scrolled, we add a class to style the editbar differently
$('iframe[name="ace_outer"]').contents().on('scroll', (ev) => { $('iframe[name="ace_outer"]').contents().on('scroll', (ev) => {
$('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop()! > 2);
}); });
} }
isEnabled() { return true; } isEnabled() { return true; }
@ -180,25 +197,25 @@ exports.padeditbar = new class {
enable() { enable() {
$('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar');
} }
registerCommand(cmd, callback) { registerCommand(cmd: string, callback: (cmd: string, ace: Ace2Editor, item: ToolbarItem)=>void) {
this.commands[cmd] = callback; this.commands[cmd] = callback;
return this; return this;
} }
registerDropdownCommand(cmd, dropdown) { registerDropdownCommand(cmd: string, dropdown?: string) {
dropdown = dropdown || cmd; dropdown = dropdown || cmd;
this.dropdowns.push(dropdown); this.dropdowns.push(dropdown);
this.registerCommand(cmd, () => { this.registerCommand(cmd, () => {
this.toggleDropDown(dropdown); this.toggleDropDown(dropdown);
}); });
} }
registerAceCommand(cmd, callback) { registerAceCommand(cmd: string, callback: ToolbarAceCallback) {
this.registerCommand(cmd, (cmd, ace, item) => { this.registerCommand(cmd, (cmd, ace, item) => {
ace.callWithAce((ace) => { ace.callWithAce((ace) => {
callback(cmd, ace, item); callback(cmd, ace, item);
}, cmd, true); }, cmd, true);
}); });
} }
triggerCommand(cmd, item) { triggerCommand(cmd: string, item: ToolbarItem) {
if (this.isEnabled() && this.commands[cmd]) { if (this.isEnabled() && this.commands[cmd]) {
this.commands[cmd](cmd, padeditor.ace, item); this.commands[cmd](cmd, padeditor.ace, item);
} }
@ -206,8 +223,8 @@ exports.padeditbar = new class {
} }
// cb is deprecated (this function is synchronous so a callback is unnecessary). // cb is deprecated (this function is synchronous so a callback is unnecessary).
toggleDropDown(moduleName, cb = null) { toggleDropDown(moduleName: string, cb:Function|null = null) {
let cbErr = null; let cbErr: Error|null = null;
try { try {
// do nothing if users are sticked // do nothing if users are sticked
if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) {
@ -249,12 +266,13 @@ exports.padeditbar = new class {
} }
} }
} catch (err) { } catch (err) {
// @ts-ignore
cbErr = err || new Error(err); cbErr = err || new Error(err);
} finally { } finally {
if (cb) Promise.resolve().then(() => cb(cbErr)); if (cb) Promise.resolve().then(() => cb(cbErr));
} }
} }
setSyncStatus(status) { setSyncStatus(status: string) {
if (status === 'syncing') { if (status === 'syncing') {
syncAnimation.syncing(); syncAnimation.syncing();
} else if (status === 'done') { } else if (status === 'done') {
@ -269,7 +287,7 @@ exports.padeditbar = new class {
if ($('#readonlyinput').is(':checked')) { if ($('#readonlyinput').is(':checked')) {
const urlParts = padUrl.split('/'); const urlParts = padUrl.split('/');
urlParts.pop(); urlParts.pop();
const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`; const readonlyLink = `${urlParts.join('/')}/${window.clientVars.readOnlyId}`;
$('#embedinput') $('#embedinput')
.val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`); .val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`);
$('#linkinput').val(readonlyLink); $('#linkinput').val(readonlyLink);
@ -288,16 +306,16 @@ exports.padeditbar = new class {
// this is approximate, we cannot measure it because on mobile // this is approximate, we cannot measure it because on mobile
// Layout it takes the full width on the bottom of the page // Layout it takes the full width on the bottom of the page
const menuRightWidth = 280; const menuRightWidth = 280;
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width() - menuRightWidth || if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()! - menuRightWidth ||
$('.toolbar').width() < 1000) { $('.toolbar').width()! < 1000) {
$('body').addClass('mobile-layout'); $('body').addClass('mobile-layout');
} }
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()) { if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()!) {
$('.toolbar').addClass('cropped'); $('.toolbar').addClass('cropped');
} }
} }
_bodyKeyEvent(evt) { _bodyKeyEvent(evt: JQuery.KeyDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) {
// If the event is Alt F9 or Escape & we're already in the editbar menu // If the event is Alt F9 or Escape & we're already in the editbar menu
// Send the users focus back to the pad // Send the users focus back to the pad
if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) { if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) {
@ -313,15 +331,17 @@ exports.padeditbar = new class {
// Timeslider probably.. // Timeslider probably..
$('#editorcontainerbox').trigger('focus'); // Focus back onto the pad $('#editorcontainerbox').trigger('focus'); // Focus back onto the pad
} else { } else {
padeditor.ace.focus(); // Sends focus back to pad padeditor.ace!.focus(); // Sends focus back to pad
// The above focus doesn't always work in FF, you have to hit enter afterwards // The above focus doesn't always work in FF, you have to hit enter afterwards
evt.preventDefault(); evt.preventDefault();
} }
} else { } else {
// Focus on the editbar :) // Focus on the editbar :)
const firstEditbarElement = $('#editbar button').first(); const firstEditbarElement = $('#editbar button').first();
// @ts-ignore
const evTarget:JQuery<HTMLElement> = $(evt.currentTarget) as any
$(evt.currentTarget).trigger('blur'); evTarget.trigger('blur');
firstEditbarElement.trigger('focus'); firstEditbarElement.trigger('focus');
evt.preventDefault(); evt.preventDefault();
} }
@ -337,7 +357,8 @@ exports.padeditbar = new class {
// On left arrow move to next button in editbar // On left arrow move to next button in editbar
if (evt.keyCode === 37) { if (evt.keyCode === 37) {
// If a dropdown is visible or we're in an input don't move to the next button // If a dropdown is visible or we're in an input don't move to the next button
if ($('.popup').is(':visible') || evt.target.localName === 'input') return; // @ts-ignore
if ($('.popup').is(':visible') || evt.target!.localName === 'input') return;
this._editbarPosition--; this._editbarPosition--;
// Allow focus to shift back to end of row and start of row // Allow focus to shift back to end of row and start of row
@ -348,6 +369,7 @@ exports.padeditbar = new class {
// On right arrow move to next button in editbar // On right arrow move to next button in editbar
if (evt.keyCode === 39) { if (evt.keyCode === 39) {
// If a dropdown is visible or we're in an input don't move to the next button // If a dropdown is visible or we're in an input don't move to the next button
// @ts-ignore
if ($('.popup').is(':visible') || evt.target.localName === 'input') return; if ($('.popup').is(':visible') || evt.target.localName === 'input') return;
this._editbarPosition++; this._editbarPosition++;
@ -394,14 +416,14 @@ exports.padeditbar = new class {
}); });
this.registerCommand('savedRevision', () => { this.registerCommand('savedRevision', () => {
padsavedrevs.saveNow(); padsavedrevs.saveNow(pad);
}); });
this.registerCommand('showTimeSlider', () => { this.registerCommand('showTimeSlider', () => {
document.location = `${document.location.pathname}/timeslider`; document.location = `${document.location.pathname}/timeslider`;
}); });
const aceAttributeCommand = (cmd, ace) => { const aceAttributeCommand = (cmd: string, ace: any) => {
ace.ace_toggleAttributeOnSelection(cmd); ace.ace_toggleAttributeOnSelection(cmd);
}; };
this.registerAceCommand('bold', aceAttributeCommand); this.registerAceCommand('bold', aceAttributeCommand);
@ -479,4 +501,6 @@ exports.padeditbar = new class {
} }
}); });
} }
}(); };
export const padeditbar = new PadEditor()

View file

@ -31,12 +31,13 @@ import {padUtils as padutils} from "./pad_utils";
import {Ace2Editor} from "./ace"; import {Ace2Editor} from "./ace";
import html10n from '../js/vendors/html10n' import html10n from '../js/vendors/html10n'
import {MapArrayType} from "../../node/types/MapType"; import {MapArrayType} from "../../node/types/MapType";
import {ClientVarData, ClientVarMessage} from "./types/SocketIOMessage"; import {ClientVarPayload, PadOption} from "./types/SocketIOMessage";
import {Pad} from "./pad";
export class PadEditor { export class PadEditor {
private pad?: PadType private pad?: Pad
private settings: undefined| ClientVarData private settings: undefined| PadOption
private ace: any ace: Ace2Editor|null
private viewZoom: number private viewZoom: number
constructor() { constructor() {
@ -47,7 +48,7 @@ export class PadEditor {
this.viewZoom = 100 this.viewZoom = 100
} }
init = async (initialViewOptions: MapArrayType<string>, _pad: PadType) => { init = async (initialViewOptions: MapArrayType<boolean>, _pad: Pad) => {
this.pad = _pad; this.pad = _pad;
this.settings = this.pad.settings; this.settings = this.pad.settings;
this.ace = new Ace2Editor(); this.ace = new Ace2Editor();
@ -125,7 +126,7 @@ export class PadEditor {
}); });
} }
setViewOptions = (newOptions: MapArrayType<string>) => { setViewOptions = (newOptions: MapArrayType<boolean>) => {
const getOption = (key: string, defaultValue: boolean) => { const getOption = (key: string, defaultValue: boolean) => {
const value = String(newOptions[key]); const value = String(newOptions[key]);
if (value === 'true') return true; if (value === 'true') return true;
@ -136,25 +137,25 @@ export class PadEditor {
let v; let v;
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection())); v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
this.ace.setProperty('rtlIsTrue', v); this.ace!.setProperty('rtlIsTrue', v);
padutils.setCheckbox($('#options-rtlcheck'), v); padutils.setCheckbox($('#options-rtlcheck'), v);
v = getOption('showLineNumbers', true); v = getOption('showLineNumbers', true);
this.ace.setProperty('showslinenumbers', v); this.ace!.setProperty('showslinenumbers', v);
padutils.setCheckbox($('#options-linenoscheck'), v); padutils.setCheckbox($('#options-linenoscheck'), v);
v = getOption('showAuthorColors', true); v = getOption('showAuthorColors', true);
this.ace.setProperty('showsauthorcolors', v); this.ace!.setProperty('showsauthorcolors', v);
$('#chattext').toggleClass('authorColors', v); $('#chattext').toggleClass('authorColors', v);
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v); $('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
padutils.setCheckbox($('#options-colorscheck'), v); padutils.setCheckbox($('#options-colorscheck'), v);
// Override from parameters if true // Override from parameters if true
if (this.settings!.noColors !== false) { if (this.settings!.noColors !== false) {
this.ace.setProperty('showsauthorcolors', !settings.noColors); this.ace!.setProperty('showsauthorcolors', !this.settings!.noColors);
} }
this.ace.setProperty('textface', newOptions.padFontFamily || ''); this.ace!.setProperty('textface', newOptions.padFontFamily || '');
} }
dispose = () => { dispose = () => {
@ -173,12 +174,12 @@ export class PadEditor {
this.ace.setEditable(false); this.ace.setEditable(false);
} }
} }
restoreRevisionText= (dataFromServer: ClientVarData) => { restoreRevisionText= (dataFromServer: ClientVarPayload) => {
this.pad!.addHistoricalAuthors(dataFromServer.historicalAuthorData); this.pad!.addHistoricalAuthors(dataFromServer.historicalAuthorData);
this.ace.importAText(dataFromServer.atext, dataFromServer.apool, true); this.ace!.importAText(dataFromServer.atext, dataFromServer.apool, true);
} }
focusOnLine = (ace) => { focusOnLine = (ace: Ace2Editor) => {
// If a number is in the URI IE #L124 go to that line number // If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1); const lineNumber = window.location.hash.substr(1);
if (lineNumber) { if (lineNumber) {

View file

@ -23,29 +23,27 @@
*/ */
import html10n from './vendors/html10n'; import html10n from './vendors/html10n';
import {Pad} from "./pad";
class PadImpExp {
const padimpexp = (() => { private pad?: Pad;
let pad;
// /// import // /// import
const addImportFrames = () => { addImportFrames = () => {
$('#import .importframe').remove(); $('#import .importframe').remove();
const iframe = $('<iframe>') const iframe = $('<iframe>')
.css('display', 'none') .css('display', 'none')
.attr('name', 'importiframe') .attr('name', 'importiframe')
.addClass('importframe'); .addClass('importframe');
$('#import').append(iframe); $('#import').append(iframe);
}; }
fileInputUpdated = () => {
const fileInputUpdated = () => {
$('#importsubmitinput').addClass('throbbold'); $('#importsubmitinput').addClass('throbbold');
$('#importformfilediv').addClass('importformenabled'); $('#importformfilediv').addClass('importformenabled');
$('#importsubmitinput').prop('disabled', false); $('#importsubmitinput').prop('disabled', false);
$('#importmessagefail').fadeOut('fast'); $('#importmessagefail').fadeOut('fast');
}; }
fileInputSubmit = (e: Event) => {
const fileInputSubmit = function (e) {
e.preventDefault(); e.preventDefault();
$('#importmessagefail').fadeOut('fast'); $('#importmessagefail').fadeOut('fast');
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return; if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return;
@ -54,10 +52,13 @@ const padimpexp = (() => {
$('#importarrow').stop(true, true).hide(); $('#importarrow').stop(true, true).hide();
$('#importstatusball').show(); $('#importstatusball').show();
(async () => { (async () => {
// @ts-ignore
const {code, message, data: {directDatabaseAccess} = {}} = await $.ajax({ const {code, message, data: {directDatabaseAccess} = {}} = await $.ajax({
url: `${window.location.href.split('?')[0].split('#')[0]}/import`, url: `${window.location.href.split('?')[0].split('#')[0]}/import`,
method: 'POST', method: 'POST',
data: new FormData(this), // FIXME is this correct
// @ts-ignore
data: new FormData(this.fileInputSubmit),
processData: false, processData: false,
contentType: false, contentType: false,
dataType: 'json', dataType: 'json',
@ -67,7 +68,7 @@ const padimpexp = (() => {
return {code: 2, message: 'Unknown import error'}; return {code: 2, message: 'Unknown import error'};
}); });
if (code !== 0) { if (code !== 0) {
importErrorMessage(message); this.importErrorMessage(message);
} else { } else {
$('#import_export').removeClass('popup-show'); $('#import_export').removeClass('popup-show');
if (directDatabaseAccess) window.location.reload(); if (directDatabaseAccess) window.location.reload();
@ -75,11 +76,11 @@ const padimpexp = (() => {
$('#importsubmitinput').prop('disabled', false).val(html10n.get('pad.impexp.importbutton')); $('#importsubmitinput').prop('disabled', false).val(html10n.get('pad.impexp.importbutton'));
window.setTimeout(() => $('#importfileinput').prop('disabled', false), 0); window.setTimeout(() => $('#importfileinput').prop('disabled', false), 0);
$('#importstatusball').hide(); $('#importstatusball').hide();
addImportFrames(); this.addImportFrames();
})(); })();
}; }
const importErrorMessage = (status) => { importErrorMessage = (status: string) => {
const known = [ const known = [
'convertFailed', 'convertFailed',
'uploadFailed', 'uploadFailed',
@ -89,12 +90,12 @@ const padimpexp = (() => {
]; ];
const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`); const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`);
const showError = (fade) => { const showError = (fade?: boolean) => {
const popup = $('#importmessagefail').empty() const popup = $('#importmessagefail').empty()
.append($('<strong>') .append($('<strong>')
.css('color', 'red') .css('color', 'red')
.text(`${html10n.get('pad.impexp.importfailed')}: `)) .text(`${html10n.get('pad.impexp.importfailed')}: `))
.append(document.createTextNode(msg)); .append(document.createTextNode(msg));
popup[(fade ? 'fadeIn' : 'show')](); popup[(fade ? 'fadeIn' : 'show')]();
}; };
@ -104,83 +105,83 @@ const padimpexp = (() => {
} else { } else {
showError(); showError();
} }
}; }
// /// export // /// export
cantExport = () => {
function cantExport() {
let type = $(this); let type = $(this);
if (type.hasClass('exporthrefpdf')) { if (type.hasClass('exporthrefpdf')) {
// @ts-ignore
type = 'PDF'; type = 'PDF';
} else if (type.hasClass('exporthrefdoc')) { } else if (type.hasClass('exporthrefdoc')) {
// @ts-ignore
type = 'Microsoft Word'; type = 'Microsoft Word';
} else if (type.hasClass('exporthrefodt')) { } else if (type.hasClass('exporthrefodt')) {
// @ts-ignore
type = 'OpenDocument'; type = 'OpenDocument';
} else { } else {
// @ts-ignore
type = 'this file'; type = 'this file';
} }
alert(html10n.get('pad.impexp.exportdisabled', {type})); alert(html10n.get('pad.impexp.exportdisabled', {type}));
return false; return false;
} }
// /// init = (_pad: Pad) => {
const self = { this.pad = _pad;
init: (_pad) => {
pad = _pad;
// get /p/padname // get /p/padname
// if /p/ isn't available due to a rewrite we use the clientVars padId // if /p/ isn't available due to a rewrite we use the clientVars padId
const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId; const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || window.clientVars.padId;
// i10l buttom import // i10l buttom import
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
html10n.bind('localized', () => {
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton')); $('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
html10n.bind('localized', () => { });
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
});
// build the export links // build the export links
$('#exporthtmla').attr('href', `${padRootPath}/export/html`); $('#exporthtmla').attr('href', `${padRootPath}/export/html`);
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`); $('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
$('#exportplaina').attr('href', `${padRootPath}/export/txt`); $('#exportplaina').attr('href', `${padRootPath}/export/txt`);
// hide stuff thats not avaible if abiword/soffice is disabled // hide stuff thats not avaible if abiword/soffice is disabled
if (clientVars.exportAvailable === 'no') { if (window.clientVars.exportAvailable === 'no') {
$('#exportworda').remove(); $('#exportworda').remove();
$('#exportpdfa').remove(); $('#exportpdfa').remove();
$('#exportopena').remove(); $('#exportopena').remove();
$('#importmessageabiword').show(); $('#importmessageabiword').show();
} else if (clientVars.exportAvailable === 'withoutPDF') { } else if (window.clientVars.exportAvailable === 'withoutPDF') {
$('#exportpdfa').remove(); $('#exportpdfa').remove();
$('#exportworda').attr('href', `${padRootPath}/export/doc`); $('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`); $('#exportopena').attr('href', `${padRootPath}/export/odt`);
$('#importexport').css({height: '142px'}); $('#importexport').css({height: '142px'});
$('#importexportline').css({height: '142px'}); $('#importexportline').css({height: '142px'});
} else { } else {
$('#exportworda').attr('href', `${padRootPath}/export/doc`); $('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`); $('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`); $('#exportopena').attr('href', `${padRootPath}/export/odt`);
} }
addImportFrames(); this.addImportFrames();
$('#importfileinput').on('change', fileInputUpdated); $('#importfileinput').on('change', this.fileInputUpdated);
$('#importform').off('submit').on('submit', fileInputSubmit); $('#importform').off('submit').on('submit', this.fileInputSubmit);
$('.disabledexport').on('click', cantExport); $('.disabledexport').on('click', this.cantExport);
}, }
disable: () => {
$('#impexp-disabled-clickcatcher').show();
$('#import').css('opacity', 0.5);
$('#impexp-export').css('opacity', 0.5);
},
enable: () => {
$('#impexp-disabled-clickcatcher').hide();
$('#import').css('opacity', 1);
$('#impexp-export').css('opacity', 1);
},
};
return self;
})();
exports.padimpexp = padimpexp; disable= () => {
$('#impexp-disabled-clickcatcher').show();
$('#import').css('opacity', 0.5);
$('#impexp-export').css('opacity', 0.5);
}
enable= () => {
$('#impexp-disabled-clickcatcher').hide();
$('#import').css('opacity', 1);
$('#impexp-export').css('opacity', 1);
}
}
export const padImpExp = new PadImpExp();

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {Pad} from "./pad";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -23,33 +25,34 @@
*/ */
const padeditbar = require('./pad_editbar').padeditbar; const padeditbar = require('./pad_editbar').padeditbar;
const automaticReconnect = require('./pad_automatic_reconnect'); import {showCountDownTimerToReconnectOnModal} from './pad_automatic_reconnect'
const padmodals = (() => { class PadModals {
let pad = undefined; private pad?: Pad
const self = {
init: (_pad) => {
pad = _pad;
},
showModal: (messageId) => {
padeditbar.toggleDropDown('none');
$('#connectivity .visible').removeClass('visible');
$(`#connectivity .${messageId}`).addClass('visible');
const $modal = $(`#connectivity .${messageId}`); constructor() {
automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); }
padeditbar.toggleDropDown('connectivity'); init = (pad: Pad) => {
}, this.pad = pad
showOverlay: () => { }
// Prevent the user to interact with the toolbar. Useful when user is disconnected for example showModal = (messageId: string) => {
$('#toolbar-overlay').show(); padeditbar.toggleDropDown('none');
}, $('#connectivity .visible').removeClass('visible');
hideOverlay: () => { $(`#connectivity .${messageId}`).addClass('visible');
$('#toolbar-overlay').hide();
},
};
return self;
})();
exports.padmodals = padmodals; const $modal = $(`#connectivity .${messageId}`);
showCountDownTimerToReconnectOnModal($modal, this.pad!);
padeditbar.toggleDropDown('connectivity');
}
showOverlay = () => {
// Prevent the user to interact with the toolbar. Useful when user is disconnected for example
$('#toolbar-overlay').show();
}
hideOverlay = () => {
$('#toolbar-overlay').hide();
}
}
export const padModals = new PadModals()

View file

@ -1,5 +1,8 @@
'use strict'; 'use strict';
import html10n from "./vendors/html10n";
import {Pad} from "./pad";
/** /**
* Copyright 2012 Peter 'Pita' Martischka * Copyright 2012 Peter 'Pita' Martischka
* *
@ -16,10 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
let pad; export const saveNow = (pad: Pad) => {
pad!.collabClient!.sendMessage({type: 'SAVE_REVISION'});
exports.saveNow = () => {
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
window.$.gritter.add({ window.$.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: html10n.get('pad.savedrevs.marked'), title: html10n.get('pad.savedrevs.marked'),
@ -32,7 +33,3 @@ exports.saveNow = () => {
class_name: 'saved-revision', class_name: 'saved-revision',
}); });
}; };
exports.init = (_pad) => {
pad = _pad;
};

View file

@ -1,610 +0,0 @@
'use strict';
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
import {padUtils as padutils} from "./pad_utils";
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
let myUserInfo = {};
let colorPickerOpen = false;
let colorPickerSetup = false;
const paduserlist = (() => {
const rowManager = (() => {
// The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height
// and TD opacity.
const nextRowId = () => `usertr${nextRowId.counter++}`;
nextRowId.counter = 1;
// objects are shared; fields are "domId","data","animationStep"
const rowsFadingOut = []; // unordered set
const rowsFadingIn = []; // unordered set
const rowsPresent = []; // in order
const ANIMATION_START = -12; // just starting to fade in
const ANIMATION_END = 12; // just finishing fading out
const animateStep = () => {
// animation must be symmetrical
for (let i = rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal
const row = rowsFadingIn[i];
const step = ++row.animationStep;
const animHeight = getAnimationHeight(step, row.animationPower);
const node = rowNode(row);
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -OPACITY_STEPS) {
node.find('td').height(animHeight);
} else if (step === -OPACITY_STEPS + 1) {
node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1 / OPACITY_STEPS);
} else if (step < 0) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS)
.height(animHeight);
} else if (step === 0) {
// set HTML in case modified during animation
node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1).height(animHeight);
rowsFadingIn.splice(i, 1); // remove from set
}
}
for (let i = rowsFadingOut.length - 1; i >= 0; i--) { // backwards to allow removal
const row = rowsFadingOut[i];
const step = ++row.animationStep;
const node = rowNode(row);
const animHeight = getAnimationHeight(step, row.animationPower);
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step < OPACITY_STEPS) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS)
.height(animHeight);
} else if (step === OPACITY_STEPS) {
node.empty().append(createEmptyRowTds(animHeight));
} else if (step <= ANIMATION_END) {
node.find('td').height(animHeight);
} else {
rowsFadingOut.splice(i, 1); // remove from set
node.remove();
}
}
handleOtherUserInputs();
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
};
const getAnimationHeight = (step, power) => {
let a = Math.abs(step / 12);
if (power === 2) a **= 2;
else if (power === 3) a **= 3;
else if (power === 4) a **= 4;
else if (power >= 5) a **= 5;
return Math.round(26 * (1 - a));
};
const OPACITY_STEPS = 6;
const ANIMATION_STEP_TIME = 20;
const LOWER_FRAMERATE_FACTOR = 2;
const {scheduleAnimation} =
padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR);
const NUMCOLS = 4;
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
// IE's poor handling when manipulating the DOM directly.
const createEmptyRowTds = (height) => $('<td>')
.attr('colspan', NUMCOLS)
.css('border', 0)
.css('height', `${height}px`);
const isNameEditable = (data) => (!data.name) && (data.status !== 'Disconnected');
const replaceUserRowContents = (tr, height, data) => {
const tds = createUserRowTds(height, data);
if (isNameEditable(data) && tr.find('td.usertdname input:enabled').length > 0) {
// preserve input field node
tds.each((i, td) => {
const oldTd = $(tr.find('td').get(i));
if (!oldTd.hasClass('usertdname')) {
oldTd.replaceWith(td);
} else {
// Prevent leak. I'm not 100% confident that this is necessary, but it shouldn't hurt.
$(td).remove();
}
});
} else {
tr.empty().append(tds);
}
return tr;
};
const createUserRowTds = (height, data) => {
let name;
if (data.name) {
name = document.createTextNode(data.name);
} else {
name = $('<input>')
.attr('data-l10n-id', 'pad.userlist.unnamed')
.attr('type', 'text')
.addClass('editempty')
.addClass('newinput')
.attr('value', html10n.get('pad.userlist.unnamed'));
if (isNameEditable(data)) name.attr('disabled', 'disabled');
}
return $()
.add($('<td>')
.css('height', `${height}px`)
.addClass('usertdswatch')
.append($('<div>')
.addClass('swatch')
.css('background', padutils.escapeHtml(data.color))
.html('&nbsp;')))
.add($('<td>')
.css('height', `${height}px`)
.addClass('usertdname')
.append(name))
.add($('<td>')
.css('height', `${height}px`)
.addClass('activity')
.text(data.activity));
};
const createRow = (id, contents, authorId) => $('<tr>')
.attr('data-authorId', authorId)
.attr('id', id)
.append(contents);
const rowNode = (row) => $(`#${row.domId}`);
const handleRowData = (row) => {
if (row.data && row.data.status === 'Disconnected') {
row.opacity = 0.5;
} else {
delete row.opacity;
}
};
const handleOtherUserInputs = () => {
// handle 'INPUT' elements for naming other unnamed users
$('#otheruserstable input.newinput').each(function () {
const input = $(this);
const tr = input.closest('tr');
if (tr.length > 0) {
const index = tr.parent().children().index(tr);
if (index >= 0) {
const userId = rowsPresent[index].data.id;
rowManagerMakeNameEditor($(this), userId);
}
}
}).removeClass('newinput');
};
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
const insertRow = (position, data, animationPower) => {
position = Math.max(0, Math.min(rowsPresent.length, position));
animationPower = (animationPower === undefined ? 4 : animationPower);
const domId = nextRowId();
const row = {
data,
animationStep: ANIMATION_START,
domId,
animationPower,
};
const authorId = data.id;
handleRowData(row);
rowsPresent.splice(position, 0, row);
let tr;
if (animationPower === 0) {
tr = createRow(domId, createUserRowTds(getAnimationHeight(0), data), authorId);
row.animationStep = 0;
} else {
rowsFadingIn.push(row);
tr = createRow(domId, createEmptyRowTds(getAnimationHeight(ANIMATION_START)), authorId);
}
$('table#otheruserstable').show();
if (position === 0) {
$('table#otheruserstable').prepend(tr);
} else {
rowNode(rowsPresent[position - 1]).after(tr);
}
if (animationPower !== 0) {
scheduleAnimation();
}
handleOtherUserInputs();
return row;
};
const updateRow = (position, data) => {
const row = rowsPresent[position];
if (row) {
row.data = data;
handleRowData(row);
if (row.animationStep === 0) {
// not currently animating
const tr = rowNode(row);
replaceUserRowContents(tr, getAnimationHeight(0), row.data)
.find('td')
.css('opacity', (row.opacity === undefined ? 1 : row.opacity));
handleOtherUserInputs();
}
}
};
const removeRow = (position, animationPower) => {
animationPower = (animationPower === undefined ? 4 : animationPower);
const row = rowsPresent[position];
if (row) {
rowsPresent.splice(position, 1); // remove
if (animationPower === 0) {
rowNode(row).remove();
} else {
row.animationStep = -row.animationStep; // use symmetry
row.animationPower = animationPower;
rowsFadingOut.push(row);
scheduleAnimation();
}
}
if (rowsPresent.length === 0) {
$('table#otheruserstable').hide();
}
};
// newPosition is position after the row has been removed
const moveRow = (oldPosition, newPosition, animationPower) => {
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
const row = rowsPresent[oldPosition];
if (row && oldPosition !== newPosition) {
const rowData = row.data;
removeRow(oldPosition, animationPower);
insertRow(newPosition, rowData, animationPower);
}
};
const self = {
insertRow,
removeRow,
moveRow,
updateRow,
};
return self;
})(); // //////// rowManager
const otherUsersInfo = [];
const otherUsersData = [];
const rowManagerMakeNameEditor = (jnode, userId) => {
setUpEditable(jnode, () => {
const existingIndex = findExistingIndex(userId);
if (existingIndex >= 0) {
return otherUsersInfo[existingIndex].name || '';
} else {
return '';
}
}, (newName) => {
if (!newName) {
jnode.addClass('editempty');
jnode.val(html10n.get('pad.userlist.unnamed'));
} else {
jnode.attr('disabled', 'disabled');
pad.suggestUserName(userId, newName);
}
});
};
const findExistingIndex = (userId) => {
let existingIndex = -1;
for (let i = 0; i < otherUsersInfo.length; i++) {
if (otherUsersInfo[i].userId === userId) {
existingIndex = i;
break;
}
}
return existingIndex;
};
const setUpEditable = (jqueryNode, valueGetter, valueSetter) => {
jqueryNode.on('focus', (evt) => {
const oldValue = valueGetter();
if (jqueryNode.val() !== oldValue) {
jqueryNode.val(oldValue);
}
jqueryNode.addClass('editactive').removeClass('editempty');
});
jqueryNode.on('blur', (evt) => {
const newValue = jqueryNode.removeClass('editactive').val();
valueSetter(newValue);
});
padutils.bindEnterAndEscape(jqueryNode, () => {
jqueryNode.trigger('blur');
}, () => {
jqueryNode.val(valueGetter()).trigger('blur');
});
jqueryNode.prop('disabled', false).addClass('editable');
};
let pad = undefined;
const self = {
init: (myInitialUserInfo, _pad) => {
pad = _pad;
self.setMyUserInfo(myInitialUserInfo);
if ($('#online_count').length === 0) {
$('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>');
}
$('#otheruserstable tr').remove();
$('#myusernameedit').addClass('myusernameedithoverable');
setUpEditable($('#myusernameedit'), () => myUserInfo.name || '', (newValue) => {
myUserInfo.name = newValue;
pad.notifyChangeName(newValue);
// wrap with setTimeout to do later because we get
// a double "blur" fire in IE...
window.setTimeout(() => {
self.renderMyUserInfo();
}, 0);
});
// color picker
$('#myswatchbox').on('click', showColorPicker);
$('#mycolorpicker .pickerswatchouter').on('click', function () {
$('#mycolorpicker .pickerswatchouter').removeClass('picked');
$(this).addClass('picked');
});
$('#mycolorpickersave').on('click', () => {
closeColorPicker(true);
});
$('#mycolorpickercancel').on('click', () => {
closeColorPicker(false);
});
//
},
usersOnline: () => {
// Returns an object of users who are currently online on this pad
// Make a copy of the otherUsersInfo, otherwise every call to users
// modifies the referenced array
const userList = [].concat(otherUsersInfo);
// Now we need to add ourselves..
userList.push(myUserInfo);
return userList;
},
users: () => {
// Returns an object of users who have been on this pad
const userList = self.usersOnline();
// Now we add historical authors
const historical = clientVars.collab_client_vars.historicalAuthorData;
for (const [key, {userId}] of Object.entries(historical)) {
// Check we don't already have this author in our array
let exists = false;
userList.forEach((user) => {
if (user.userId === userId) exists = true;
});
if (exists === false) {
userList.push(historical[key]);
}
}
return userList;
},
setMyUserInfo: (info) => {
// translate the colorId
if (typeof info.colorId === 'number') {
info.colorId = clientVars.colorPalette[info.colorId];
}
myUserInfo = $.extend(
{}, info);
self.renderMyUserInfo();
},
userJoinOrUpdate: (info) => {
if ((!info.userId) || (info.userId === myUserInfo.userId)) {
// not sure how this would happen
return;
}
hooks.callAll('userJoinOrUpdate', {
userInfo: info,
});
const userData = {};
userData.color = typeof info.colorId === 'number'
? clientVars.colorPalette[info.colorId] : info.colorId;
userData.name = info.name;
userData.status = '';
userData.activity = '';
userData.id = info.userId;
const existingIndex = findExistingIndex(info.userId);
let numUsersBesides = otherUsersInfo.length;
if (existingIndex >= 0) {
numUsersBesides--;
}
const newIndex = padutils.binarySearch(numUsersBesides, (n) => {
if (existingIndex >= 0 && n >= existingIndex) {
// pretend existingIndex isn't there
n++;
}
const infoN = otherUsersInfo[n];
const nameN = (infoN.name || '').toLowerCase();
const nameThis = (info.name || '').toLowerCase();
const idN = infoN.userId;
const idThis = info.userId;
return (nameN > nameThis) || (nameN === nameThis && idN > idThis);
});
if (existingIndex >= 0) {
// update
if (existingIndex === newIndex) {
otherUsersInfo[existingIndex] = info;
otherUsersData[existingIndex] = userData;
rowManager.updateRow(existingIndex, userData);
} else {
otherUsersInfo.splice(existingIndex, 1);
otherUsersData.splice(existingIndex, 1);
otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData);
rowManager.updateRow(existingIndex, userData);
rowManager.moveRow(existingIndex, newIndex);
}
} else {
otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData);
rowManager.insertRow(newIndex, userData);
}
self.updateNumberOfOnlineUsers();
},
updateNumberOfOnlineUsers: () => {
let online = 1; // you are always online!
for (let i = 0; i < otherUsersData.length; i++) {
if (otherUsersData[i].status === '') {
online++;
}
}
$('#online_count').text(online);
return online;
},
userLeave: (info) => {
const existingIndex = findExistingIndex(info.userId);
if (existingIndex >= 0) {
const userData = otherUsersData[existingIndex];
userData.status = 'Disconnected';
rowManager.updateRow(existingIndex, userData);
if (userData.leaveTimer) {
window.clearTimeout(userData.leaveTimer);
}
// set up a timer that will only fire if no leaves,
// joins, or updates happen for this user in the
// next N seconds, to remove the user from the list.
const thisUserId = info.userId;
const thisLeaveTimer = window.setTimeout(() => {
const newExistingIndex = findExistingIndex(thisUserId);
if (newExistingIndex >= 0) {
const newUserData = otherUsersData[newExistingIndex];
if (newUserData.status === 'Disconnected' &&
newUserData.leaveTimer === thisLeaveTimer) {
otherUsersInfo.splice(newExistingIndex, 1);
otherUsersData.splice(newExistingIndex, 1);
rowManager.removeRow(newExistingIndex);
hooks.callAll('userLeave', {
userInfo: info,
});
}
}
}, 8000); // how long to wait
userData.leaveTimer = thisLeaveTimer;
}
self.updateNumberOfOnlineUsers();
},
renderMyUserInfo: () => {
if (myUserInfo.name) {
$('#myusernameedit').removeClass('editempty').val(myUserInfo.name);
} else {
$('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername'));
}
if (colorPickerOpen) {
$('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable');
} else {
$('#myswatchbox').addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable');
}
$('#myswatch').css({'background-color': myUserInfo.colorId});
$('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}`});
},
};
return self;
})();
const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode);
const closeColorPicker = (accept) => {
if (accept) {
let newColor = $('#mycolorpickerpreview').css('background-color');
const parts = newColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
// parts now should be ["rgb(0, 70, 255", "0", "70", "255"]
if (parts) {
delete (parts[0]);
for (let i = 1; i <= 3; ++i) {
parts[i] = parseInt(parts[i]).toString(16);
if (parts[i].length === 1) parts[i] = `0${parts[i]}`;
}
newColor = `#${parts.join('')}`; // "0070ff"
}
myUserInfo.colorId = newColor;
pad.notifyChangeColor(newColor);
paduserlist.renderMyUserInfo();
} else {
// pad.notifyChangeColor(previousColorId);
// paduserlist.renderMyUserInfo();
}
colorPickerOpen = false;
$('#mycolorpicker').removeClass('popup-show');
};
const showColorPicker = () => {
$.farbtastic('#colorpicker').setColor(myUserInfo.colorId);
if (!colorPickerOpen) {
const palette = pad.getColorPalette();
if (!colorPickerSetup) {
const colorsList = $('#colorpickerswatches');
for (let i = 0; i < palette.length; i++) {
const li = $('<li>', {
style: `background: ${palette[i]};`,
});
li.appendTo(colorsList);
li.on('click', (event) => {
$('#colorpickerswatches li').removeClass('picked');
$(event.target).addClass('picked');
const newColorId = getColorPickerSwatchIndex($('#colorpickerswatches .picked'));
pad.notifyChangeColor(newColorId);
});
}
colorPickerSetup = true;
}
$('#mycolorpicker').addClass('popup-show');
colorPickerOpen = true;
$('#colorpickerswatches li').removeClass('picked');
$($('#colorpickerswatches li')[myUserInfo.colorId]).addClass('picked'); // seems weird
}
};
exports.paduserlist = paduserlist;

View file

@ -0,0 +1,661 @@
'use strict';
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
import {padUtils as padutils} from "./pad_utils";
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
import {UserInfo} from "./types/SocketIOMessage";
import {Pad} from "./pad";
let colorPickerOpen = false;
let colorPickerSetup = false;
type RowData = {
name: string
status: string
color: string
activity: string
id: string
}
type Row = {
data?: RowData
animationPower?: number,
animationStep?: number,
opacity?: number
domId?: string
}
type UserData = {
color? : number
name? : string
status? : string
activity? : string
id? : string
leaveTimer?: number
} & RowData
class RowManager {
// The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height
// and TD opacity.
nextRowIdCounter = 1;
nextRowId = () => `usertr${this.nextRowIdCounter++}`;
// objects are shared; fields are "domId","data","animationStep"
rowsFadingOut: Row[] = []; // unordered set
rowsFadingIn: Row[] = []; // unordered set
OPACITY_STEPS = 6;
ANIMATION_STEP_TIME = 20;
LOWER_FRAMERATE_FACTOR = 2;
scheduleAnimation: () => void
rowsPresent: Row[] = []; // in order
ANIMATION_START = -12; // just starting to fade in
ANIMATION_END = 12; // just finishing fading out
NUMCOLS = 4;
private padUserList: PadUserList;
constructor(p: PadUserList) {
let {scheduleAnimation} = padutils.makeAnimationScheduler(this.animateStep, this.ANIMATION_STEP_TIME, this.LOWER_FRAMERATE_FACTOR);
this.scheduleAnimation = scheduleAnimation
this.padUserList = p
}
animateStep = () => {
// animation must be symmetrical
for (let i = this.rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal
const row = this.rowsFadingIn[i];
const step = ++row.animationStep!;
const animHeight = this.getAnimationHeight(step, row.animationPower);
const node = this.rowNode(row);
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -this.OPACITY_STEPS) {
node.find('td').height(animHeight);
} else if (step === -this.OPACITY_STEPS + 1) {
node.empty().append(this.createUserRowTds(animHeight, row.data!))
.find('td').css('opacity', baseOpacity * 1 / this.OPACITY_STEPS);
} else if (step < 0) {
node.find('td').css('opacity', baseOpacity * (this.OPACITY_STEPS - (-step)) / this.OPACITY_STEPS)
.height(animHeight);
} else if (step === 0) {
// set HTML in case modified during animation
node.empty().append(this.createUserRowTds(animHeight, row.data!))
.find('td').css('opacity', baseOpacity * 1).height(animHeight);
this.rowsFadingIn.splice(i, 1); // remove from set
}
}
for (let i = this.rowsFadingOut.length - 1; i >= 0; i--) { // backwards to allow removal
const row = this.rowsFadingOut[i];
const step = ++row.animationStep!;
const node = this.rowNode(row);
const animHeight = this.getAnimationHeight(step, row.animationPower);
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step < this.OPACITY_STEPS) {
node.find('td').css('opacity', baseOpacity * (this.OPACITY_STEPS - step) / this.OPACITY_STEPS)
.height(animHeight);
} else if (step === this.OPACITY_STEPS) {
node.empty().append(this.createEmptyRowTds(animHeight));
} else if (step <= this.ANIMATION_END) {
node.find('td').height(animHeight);
} else {
this.rowsFadingOut.splice(i, 1); // remove from set
node.remove();
}
}
this.handleOtherUserInputs();
return (this.rowsFadingIn.length > 0) || (this.rowsFadingOut.length > 0); // is more to do
}
getAnimationHeight = (step: number, power?: number) => {
let a = Math.abs(step / 12);
if (power === 2) a **= 2;
else if (power === 3) a **= 3;
else if (power === 4) a **= 4;
else if (power! >= 5) a **= 5;
return Math.round(26 * (1 - a));
}
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
// IE's poor handling when manipulating the DOM directly.
createEmptyRowTds = (height: number) => $('<td>')
.attr('colspan', this.NUMCOLS)
.css('border', 0)
.css('height', `${height}px`);
isNameEditable = (data: RowData) => (!data.name) && (data.status !== 'Disconnected');
replaceUserRowContents = (tr: JQuery<HTMLElement>, height: number, data: RowData) => {
const tds = this.createUserRowTds(height, data);
if (this.isNameEditable(data) && tr.find('td.usertdname input:enabled').length > 0) {
// preserve input field node
tds.each((i, td) => {
// @ts-ignore
const oldTd = $(tr.find('td').get(i)) as JQuery<HTMLElement>;
if (!oldTd.hasClass('usertdname')) {
oldTd.replaceWith(td);
} else {
// Prevent leak. I'm not 100% confident that this is necessary, but it shouldn't hurt.
$(td).remove();
}
});
} else {
tr.empty().append(tds);
}
return tr;
}
createUserRowTds = (height: number, data: RowData) => {
let name;
if (data.name) {
name = document.createTextNode(data.name);
} else {
name = $('<input>')
.attr('data-l10n-id', 'pad.userlist.unnamed')
.attr('type', 'text')
.addClass('editempty')
.addClass('newinput')
.attr('value', html10n.get('pad.userlist.unnamed'));
if (this.isNameEditable(data)) name.attr('disabled', 'disabled');
}
return $()
.add($('<td>')
.css('height', `${height}px`)
.addClass('usertdswatch')
.append($('<div>')
.addClass('swatch')
.css('background', padutils.escapeHtml(data.color))
.html('&nbsp;')))
.add($('<td>')
.css('height', `${height}px`)
.addClass('usertdname')
.append(name))
.add($('<td>')
.css('height', `${height}px`)
.addClass('activity')
.text(data.activity));
}
createRow = (id: string, contents: JQuery<HTMLElement>, authorId: string) => $('<tr>')
.attr('data-authorId', authorId)
.attr('id', id)
.append(contents);
rowNode = (row: Row) => $(`#${row.domId}`);
handleRowData = (row: Row) => {
if (row.data && row.data.status === 'Disconnected') {
row.opacity = 0.5;
} else {
delete row.opacity;
}
}
handleOtherUserInputs = () => {
// handle 'INPUT' elements for naming other unnamed users
$('#otheruserstable input.newinput').each(() => {
const input = $(this);
// @ts-ignore
const tr = input.closest('tr') as JQuery<HTMLElement>
if (tr.length > 0) {
const index = tr.parent().children().index(tr);
if (index >= 0) {
const userId = this.rowsPresent[index].data!.id;
// @ts-ignore
this.padUserList.rowManagerMakeNameEditor($(this) as JQuery<HTMLElement>, userId);
}
}
}).removeClass('newinput');
}
insertRow = (position: number, data: RowData, animationPower?: number) => {
position = Math.max(0, Math.min(this.rowsPresent.length, position));
animationPower = (animationPower === undefined ? 4 : animationPower);
const domId = this.nextRowId();
const row = {
data,
animationStep: this.ANIMATION_START,
domId,
animationPower,
};
const authorId = data.id;
this.handleRowData(row);
this.rowsPresent.splice(position, 0, row);
let tr;
if (animationPower === 0) {
tr = this.createRow(domId, this.createUserRowTds(this.getAnimationHeight(0), data), authorId);
row.animationStep = 0;
} else {
this.rowsFadingIn.push(row);
tr = this.createRow(domId, this.createEmptyRowTds(this.getAnimationHeight(this.ANIMATION_START)), authorId);
}
$('table#otheruserstable').show();
if (position === 0) {
$('table#otheruserstable').prepend(tr);
} else {
this.rowNode(this.rowsPresent[position - 1]).after(tr);
}
if (animationPower !== 0) {
this.scheduleAnimation();
}
this.handleOtherUserInputs();
return row;
}
updateRow = (position: number, data: UserData) => {
const row = this.rowsPresent[position];
if (row) {
row.data = data;
this.handleRowData(row);
if (row.animationStep === 0) {
// not currently animating
const tr = this.rowNode(row);
this.replaceUserRowContents(tr, this.getAnimationHeight(0), row.data)
.find('td')
.css('opacity', (row.opacity === undefined ? 1 : row.opacity));
this.handleOtherUserInputs();
}
}
}
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
removeRow = (position: number, animationPower?: number) => {
animationPower = (animationPower === undefined ? 4 : animationPower);
const row = this.rowsPresent[position];
if (row) {
this.rowsPresent.splice(position, 1); // remove
if (animationPower === 0) {
this.rowNode(row).remove();
} else {
row.animationStep = -row.animationStep!; // use symmetry
row.animationPower = animationPower;
this.rowsFadingOut.push(row);
this.scheduleAnimation();
}
}
if (this.rowsPresent.length === 0) {
$('table#otheruserstable').hide();
}
}
// newPosition is position after the row has been removed
moveRow = (oldPosition: number, newPosition: number, animationPower?: number) => {
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
const row = this.rowsPresent[oldPosition];
if (row && oldPosition !== newPosition) {
const rowData = row.data;
this.removeRow(oldPosition, animationPower);
this.insertRow(newPosition, rowData!, animationPower);
}
}
}
class PadUserList {
private rowManager: RowManager;
private otherUsersInfo: UserInfo[] = [];
private otherUsersData: UserData[] = [];
private pad?: Pad = undefined;
private myUserInfo?: UserInfo
constructor() {
this.rowManager = new RowManager(this)
}
rowManagerMakeNameEditor = (jnode: JQuery<HTMLElement>, userId: string) => {
this.setUpEditable(jnode, () => {
const existingIndex = this.findExistingIndex(userId);
if (existingIndex >= 0) {
return this.otherUsersInfo[existingIndex].name || '';
} else {
return '';
}
}, (newName: string) => {
if (!newName) {
jnode.addClass('editempty');
jnode.val(html10n.get('pad.userlist.unnamed'));
} else {
jnode.attr('disabled', 'disabled');
pad.suggestUserName(userId, newName);
}
})
}
findExistingIndex = (userId: string) => {
let existingIndex = -1;
for (let i = 0; i < this.otherUsersInfo.length; i++) {
if (this.otherUsersInfo[i].userId === userId) {
existingIndex = i;
break;
}
}
return existingIndex;
}
setUpEditable = (jqueryNode: JQuery<HTMLElement>, valueGetter: () => any, valueSetter: (val: any) => void) => {
jqueryNode.on('focus', (evt) => {
const oldValue = valueGetter();
if (jqueryNode.val() !== oldValue) {
jqueryNode.val(oldValue);
}
jqueryNode.addClass('editactive').removeClass('editempty');
});
jqueryNode.on('blur', (evt) => {
const newValue = jqueryNode.removeClass('editactive').val();
valueSetter(newValue);
});
padutils.bindEnterAndEscape(jqueryNode, () => {
jqueryNode.trigger('blur');
}, () => {
jqueryNode.val(valueGetter()).trigger('blur');
});
jqueryNode.prop('disabled', false).addClass('editable');
}
init = (myInitialUserInfo: UserInfo, _pad: Pad) => {
this.pad = _pad;
this.setMyUserInfo(myInitialUserInfo);
if ($('#online_count').length === 0) {
$('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>');
}
$('#otheruserstable tr').remove();
$('#myusernameedit').addClass('myusernameedithoverable');
this.setUpEditable($('#myusernameedit'), () => this.myUserInfo!.name || '', (newValue) => {
this.myUserInfo!.name = newValue;
pad.notifyChangeName(newValue);
// wrap with setTimeout to do later because we get
// a double "blur" fire in IE...
window.setTimeout(() => {
this.renderMyUserInfo();
}, 0);
});
// color picker
$('#myswatchbox').on('click', this.showColorPicker);
$('#mycolorpicker .pickerswatchouter').on('click', function () {
$('#mycolorpicker .pickerswatchouter').removeClass('picked');
$(this).addClass('picked');
});
$('#mycolorpickersave').on('click', () => {
this.closeColorPicker(true);
});
$('#mycolorpickercancel').on('click', () => {
this.closeColorPicker(false);
});
//
}
usersOnline = () => {
// Returns an object of users who are currently online on this pad
// Make a copy of the otherUsersInfo, otherwise every call to users
// modifies the referenced array
let newConcat: UserInfo[] = []
const userList: UserInfo[] = newConcat.concat(this.otherUsersInfo);
// Now we need to add ourselves..
userList.push(this.myUserInfo!);
return userList;
}
users = () => {
// Returns an object of users who have been on this pad
const userList = this.usersOnline();
// Now we add historical authors
const historical = window.clientVars.collab_client_vars.historicalAuthorData;
for (const [key,
{
userId
}
]
of
Object.entries(historical)
) {
// Check we don't already have this author in our array
let exists = false;
userList.forEach((user) => {
if (user.userId === userId) exists = true;
});
if (exists === false) {
userList.push(historical[key]);
}
}
return userList;
}
setMyUserInfo = (info: UserInfo) => {
// translate the colorId
if (typeof info.colorId === 'number') {
info.colorId = window.clientVars.colorPalette[info.colorId];
}
this.myUserInfo = $.extend(
{}, info);
this.renderMyUserInfo();
}
userJoinOrUpdate
=
(info: UserInfo) => {
if ((!info.userId) || (info.userId === this.myUserInfo!.userId)) {
// not sure how this would happen
return;
}
hooks.callAll('userJoinOrUpdate', {
userInfo: info,
});
// @ts-ignore
const userData: UserData = {};
// @ts-ignore
userData.color = typeof info.colorId === 'number'
? window.clientVars.colorPalette[info.colorId] : info.colorId!;
userData.name = info.name;
userData.status = '';
userData.activity = '';
userData.id = info.userId;
const existingIndex = this.findExistingIndex(info.userId);
let numUsersBesides = this.otherUsersInfo.length;
if (existingIndex >= 0) {
numUsersBesides--;
}
const newIndex = padutils.binarySearch(numUsersBesides, (n: number) => {
if (existingIndex >= 0 && n >= existingIndex) {
// pretend existingIndex isn't there
n++;
}
const infoN = this.otherUsersInfo[n];
const nameN = (infoN.name || '').toLowerCase();
const nameThis = (info.name || '').toLowerCase();
const idN = infoN.userId;
const idThis = info.userId;
return (nameN > nameThis) || (nameN === nameThis && idN > idThis);
});
if (existingIndex >= 0) {
// update
if (existingIndex === newIndex) {
this.otherUsersInfo[existingIndex] = info;
this.otherUsersData[existingIndex] = userData;
this.rowManager.updateRow(existingIndex, userData!);
} else {
this.otherUsersInfo.splice(existingIndex, 1);
this.otherUsersData.splice(existingIndex, 1);
this.otherUsersInfo.splice(newIndex, 0, info);
this.otherUsersData.splice(newIndex, 0, userData);
this.rowManager.updateRow(existingIndex, userData!);
this.rowManager.moveRow(existingIndex, newIndex);
}
} else {
this.otherUsersInfo.splice(newIndex, 0, info);
this.otherUsersData.splice(newIndex, 0, userData);
this.rowManager.insertRow(newIndex, userData);
}
this.updateNumberOfOnlineUsers();
}
updateNumberOfOnlineUsers
=
() => {
let online = 1; // you are always online!
for (let i = 0; i < this.otherUsersData.length; i++) {
if (this.otherUsersData[i].status === '') {
online++;
}
}
$('#online_count').text(online);
return online;
}
userLeave
=
(info: UserInfo) => {
const existingIndex = this.findExistingIndex(info.userId);
if (existingIndex >= 0) {
const userData = this.otherUsersData[existingIndex];
userData.status = 'Disconnected';
this.rowManager.updateRow(existingIndex, userData);
if (userData.leaveTimer) {
window.clearTimeout(userData.leaveTimer);
}
// set up a timer that will only fire if no leaves,
// joins, or updates happen for this user in the
// next N seconds, to remove the user from the list.
const thisUserId = info.userId;
const thisLeaveTimer = window.setTimeout(() => {
const newExistingIndex = this.findExistingIndex(thisUserId);
if (newExistingIndex >= 0) {
const newUserData = this.otherUsersData[newExistingIndex];
if (newUserData.status === 'Disconnected' &&
newUserData.leaveTimer === thisLeaveTimer) {
this.otherUsersInfo.splice(newExistingIndex, 1);
this.otherUsersData.splice(newExistingIndex, 1);
this.rowManager.removeRow(newExistingIndex);
hooks.callAll('userLeave', {
userInfo: info,
});
}
}
}, 8000); // how long to wait
userData.leaveTimer = thisLeaveTimer;
}
this.updateNumberOfOnlineUsers();
}
renderMyUserInfo
=
() => {
if (this.myUserInfo!.name) {
$('#myusernameedit').removeClass('editempty').val(this.myUserInfo!.name);
} else {
$('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername'));
}
if (colorPickerOpen) {
$('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable');
} else {
$('#myswatchbox').addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable');
}
$('#myswatch').css({'background-color': this.myUserInfo!.colorId});
$('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${this.myUserInfo!.colorId}`});
}
getColorPickerSwatchIndex = (jnode: JQuery<HTMLElement>) => $('#colorpickerswatches li').index(jnode)
closeColorPicker = (accept: boolean) => {
if (accept) {
let newColor = $('#mycolorpickerpreview').css('background-color');
const parts = newColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
// parts now should be ["rgb(0, 70, 255", "0", "70", "255"]
if (parts) {
// @ts-ignore
delete (parts[0]);
for (let i = 1; i <= 3; ++i) {
parts[i] = parseInt(parts[i]).toString(16);
if (parts[i].length === 1) parts[i] = `0${parts[i]}`;
}
newColor = `#${parts.join('')}`; // "0070ff"
}
// @ts-ignore
this.myUserInfo!.colorId! = newColor;
// @ts-ignore
pad.notifyChangeColor(newColor);
paduserlist.renderMyUserInfo();
} else {
// pad.notifyChangeColor(previousColorId);
// paduserlist.renderMyUserInfo();
}
colorPickerOpen = false;
$('#mycolorpicker').removeClass('popup-show');
}
showColorPicker = () => {
// @ts-ignore
$.farbtastic('#colorpicker').setColor(this.myUserInfo!.colorId);
if (!colorPickerOpen) {
const palette = pad.getColorPalette();
if (!colorPickerSetup) {
const colorsList = $('#colorpickerswatches');
for (let i = 0; i < palette.length; i++) {
const li = $('<li>', {
style: `background: ${palette[i]};`,
});
li.appendTo(colorsList);
li.on('click', (event) => {
$('#colorpickerswatches li').removeClass('picked');
$(event.target).addClass('picked');
const newColorId = this.getColorPickerSwatchIndex($('#colorpickerswatches .picked'));
pad.notifyChangeColor(newColorId);
});
}
colorPickerSetup = true;
}
$('#mycolorpicker').addClass('popup-show');
colorPickerOpen = true;
$('#colorpickerswatches li').removeClass('picked');
$($('#colorpickerswatches li')[this.myUserInfo!.colorId]).addClass('picked'); // seems weird
}
};
}
export const paduserlist = new PadUserList()

View file

@ -10,7 +10,7 @@ import {Socket} from "socket.io";
* https://socket.io/docs/v2/client-api/#new-Manager-url-options * https://socket.io/docs/v2/client-api/#new-Manager-url-options
* @return socket.io Socket object * @return socket.io Socket object
*/ */
const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): Socket => { const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): any => {
// The API for socket.io's io() function is awkward. The documentation says that the first // The API for socket.io's io() function is awkward. The documentation says that the first
// argument is a URL, but it is not the URL of the socket.io endpoint. The URL's path part is used // argument is a URL, but it is not the URL of the socket.io endpoint. The URL's path part is used
// as the name of the socket.io namespace to join, and the rest of the URL (including query // as the name of the socket.io namespace to join, and the rest of the URL (including query
@ -28,15 +28,10 @@ const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): Socke
}; };
socketOptions = Object.assign(options, socketOptions); socketOptions = Object.assign(options, socketOptions);
const socket = io(namespaceUrl.href, socketOptions) as unknown as Socket; const socket = io(namespaceUrl.href, socketOptions);
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
console.log('Error connecting to pad', error); console.log('Error connecting to pad', error);
/*if (socket.io.engine.transports.indexOf('polling') === -1) {
console.warn('WebSocket connection failed. Falling back to long-polling.');
socket.io.opts.transports = ['websocket','polling'];
socket.io.engine.upgrade = false;
}*/
}); });
return socket; return socket;

View file

@ -31,10 +31,10 @@ const hooks = require('./pluginfw/hooks');
import connect from './socketio' import connect from './socketio'
import html10n from '../js/vendors/html10n' import html10n from '../js/vendors/html10n'
import {Socket} from "socket.io"; import {Socket} from "socket.io";
import {ClientVarMessage, SocketIOMessage} from "./types/SocketIOMessage"; import {ClientVarData, ClientVarMessage, ClientVarPayload, SocketIOMessage} from "./types/SocketIOMessage";
import {Func} from "mocha"; import {Func} from "mocha";
type ChangeSetLoader = { export type ChangeSetLoader = {
handleMessageFromServer(msg: ClientVarMessage): void handleMessageFromServer(msg: ClientVarMessage): void
} }
@ -80,7 +80,7 @@ export const init = () => {
socket.on('message', (message: ClientVarMessage) => { socket.on('message', (message: ClientVarMessage) => {
if (message.type === 'CLIENT_VARS') { if (message.type === 'CLIENT_VARS') {
handleClientVars(message); handleClientVars(message);
} else if (message.accessStatus) { } else if ("accessStatus" in message) {
$('body').html('<h2>You have no permission to access this pad</h2>'); $('body').html('<h2>You have no permission to access this pad</h2>');
} else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') { } else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') {
changesetLoader.handleMessageFromServer(message); changesetLoader.handleMessageFromServer(message);
@ -111,7 +111,7 @@ const sendSocketMsg = (type: string, data: Object) => {
const fireWhenAllScriptsAreLoaded: Function[] = []; const fireWhenAllScriptsAreLoaded: Function[] = [];
const handleClientVars = (message: ClientVarMessage) => { const handleClientVars = (message: ClientVarData) => {
// save the client Vars // save the client Vars
window.clientVars = message.data; window.clientVars = message.data;

View file

@ -1,43 +1,209 @@
import {MapArrayType} from "../../../node/types/MapType"; import {MapArrayType} from "../../../node/types/MapType";
import {AText} from "./AText"; import {AText} from "./AText";
import AttributePool from "../AttributePool"; import AttributePool from "../AttributePool";
import attributePool from "../AttributePool";
export type SocketIOMessage = { export type SocketIOMessage = {
type: string type: string
accessStatus: string accessStatus: string
} }
export type ClientVarData = { export type HistoricalAuthorData = MapArrayType<{
name: string;
colorId: number;
userId: string
}>
export type ServerVar = {
rev: number
historicalAuthorData: HistoricalAuthorData,
initialAttributedText: string,
apool: AttributePool
}
export type UserInfo = {
userId: string
colorId: number,
name: string
}
export type ClientVarPayload = {
readOnlyId: string
automaticReconnectionTimeout: number
sessionRefreshInterval: number, sessionRefreshInterval: number,
historicalAuthorData:MapArrayType<{ historicalAuthorData: HistoricalAuthorData,
name: string;
colorId: string;
}>,
atext: AText, atext: AText,
apool: AttributePool, apool: AttributePool,
noColors: boolean, noColors: boolean,
userName: string, userName: string,
userColor:string, userColor: number,
hideChat: boolean, hideChat: boolean,
padOptions: MapArrayType<string>, padOptions: PadOption,
padId: string, padId: string,
clientIp: string, clientIp: string,
colorPalette: MapArrayType<string>, colorPalette: MapArrayType<number>,
accountPrivs: MapArrayType<string>, accountPrivs: MapArrayType<string>,
collab_client_vars: MapArrayType<string>, collab_client_vars: ServerVar,
chatHead: number, chatHead: number,
readonly: boolean, readonly: boolean,
serverTimestamp: number, serverTimestamp: number,
initialOptions: MapArrayType<string>, initialOptions: MapArrayType<string>,
userId: string, userId: string,
mode: string,
randomVersionString: string,
skinName: string
skinVariants: string,
exportAvailable: string
} }
export type ClientVarMessage = { export type ClientVarData = {
data: ClientVarData, type: "CLIENT_VARS"
type: string data: ClientVarPayload
accessStatus: string
} }
export type ClientNewChanges = {
type : 'NEW_CHANGES'
apool: AttributePool,
author: string,
changeset: string,
newRev: number,
payload?: ClientNewChanges
}
export type ClientAcceptCommitMessage = {
type: 'ACCEPT_COMMIT'
newRev: number
}
export type ClientConnectMessage = {
type: 'CLIENT_RECONNECT',
noChanges: boolean,
headRev: number,
newRev: number,
changeset: string,
author: string
apool: AttributePool
}
export type UserNewInfoMessage = {
type: 'USER_NEWINFO',
userInfo: UserInfo
}
export type UserLeaveMessage = {
type: 'USER_LEAVE'
userInfo: UserInfo
}
export type ClientMessageMessage = {
type: 'CLIENT_MESSAGE',
payload: ClientSendMessages
}
export type ChatMessageMessage = {
type: 'CHAT_MESSAGE'
message: string
}
export type ChatMessageMessages = {
type: 'CHAT_MESSAGES'
messages: string
}
export type ClientUserChangesMessage = {
type: 'USER_CHANGES',
baseRev: number,
changeset: string,
apool: attributePool
}
export type ClientSendMessages = ClientUserChangesMessage | ClientSendUserInfoUpdate| ClientMessageMessage | GetChatMessageMessage |ClientSuggestUserName | NewRevisionListMessage | RevisionLabel | PadOptionsMessage| ClientSaveRevisionMessage
export type ClientSaveRevisionMessage = {
type: 'SAVE_REVISION'
}
export type GetChatMessageMessage = {
type: 'GET_CHAT_MESSAGES',
start: number,
end: number
}
export type ClientSendUserInfoUpdate = {
type: 'USERINFO_UPDATE',
userInfo: UserInfo
}
export type ClientSuggestUserName = {
type: 'suggestUserName',
unnamedId: string,
newName: string
}
export type NewRevisionListMessage = {
type: 'newRevisionList',
revisionList: number[]
}
export type RevisionLabel = {
type: 'revisionLabel'
revisionList: number[]
}
export type PadOptionsMessage = {
type: 'padoptions'
options: PadOption
changedBy: string
}
export type PadOption = {
"noColors"?: boolean,
"showControls"?: boolean,
"showChat"?: boolean,
"showLineNumbers"?: boolean,
"useMonospaceFont"?: boolean,
"userName"?: null|string,
"userColor"?: null|string,
"rtl"?: boolean,
"alwaysShowChat"?: boolean,
"chatAndUsers"?: boolean,
"lang"?: null|string,
view? : MapArrayType<boolean>
}
type SharedMessageType = {
payload:{
timestamp: number
}
}
export type x = {
disconnect: boolean
}
export type ClientDisconnectedMessage = {
type: "disconnected"
disconnected: boolean
}
export type ClientVarMessage = {
type: 'CHANGESET_REQ'| 'COLLABROOM'| 'CUSTOM'
data:
| ClientNewChanges
| ClientAcceptCommitMessage
|UserNewInfoMessage
| UserLeaveMessage
|ClientMessageMessage
|ChatMessageMessage
|ChatMessageMessages
|ClientConnectMessage,
} | ClientVarData | ClientDisconnectedMessage
export type SocketClientReadyMessage = { export type SocketClientReadyMessage = {
type: string type: string
component: string component: string

View file

@ -1,9 +1,12 @@
import {ClientVarData} from "./SocketIOMessage"; import {ClientVarData, ClientVarPayload} from "./SocketIOMessage";
import {Pad} from "../pad";
declare global { declare global {
interface Window { interface Window {
clientVars: ClientVarData; clientVars: ClientVarPayload;
$: any, $: any,
customStart?:any customStart?:any,
ajlog: string
} }
let pad: Pad
} }

37
src/static/js/vendors/BrowserType.ts vendored Normal file
View file

@ -0,0 +1,37 @@
export type BrowserType = {
webos?: boolean
name: string,
opera?: boolean,
version?: number,
yandexbrowser?: boolean,
windowsphone?: boolean,
msedge?: boolean,
msie?: boolean,
chromeos?: boolean
chromeBook?: boolean
chrome?: boolean,
sailfish?: boolean,
seamonkey?: boolean,
firefox?: boolean,
firefoxos?: boolean,
silk?: boolean,
phantom?: boolean,
blackberry?: boolean
touchpad?: boolean,
bada?: boolean,
tizen?: boolean,
safari?: boolean,
webkit?: boolean,
gecko?: boolean,
android?: boolean,
ios?: boolean,
windows?: boolean
mac?: boolean
linux?: boolean
osversion?: string
tablet?: boolean
mobile?: boolean
a?: boolean
c?: boolean
x?: boolean
}

View file

@ -1,310 +0,0 @@
// WARNING: This file may have been modified from original.
// TODO: Check requirement of this file, this afaik was to cover weird edge cases
// that have probably been fixed in browsers.
/*!
* Bowser - a browser detector
* https://github.com/ded/bowser
* MIT License | (c) Dustin Diaz 2015
*/
!function (name, definition) {
if (typeof module != 'undefined' && module.exports) module.exports = definition()
else if (typeof define == 'function' && define.amd) define(definition)
else this[name] = definition()
}('bowser', function () {
/**
* See useragents.js for examples of navigator.userAgent
*/
var t = true
function detect(ua) {
function getFirstMatch(regex) {
var match = ua.match(regex);
return (match && match.length > 1 && match[1]) || '';
}
function getSecondMatch(regex) {
var match = ua.match(regex);
return (match && match.length > 1 && match[2]) || '';
}
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
, likeAndroid = /like android/i.test(ua)
, android = !likeAndroid && /android/i.test(ua)
, chromeos = /CrOS/.test(ua)
, silk = /silk/i.test(ua)
, sailfish = /sailfish/i.test(ua)
, tizen = /tizen/i.test(ua)
, webos = /(web|hpw)os/i.test(ua)
, windowsphone = /windows phone/i.test(ua)
, windows = !windowsphone && /windows/i.test(ua)
, mac = !iosdevice && !silk && /macintosh/i.test(ua)
, linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua)
, edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
, versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
, tablet = /tablet/i.test(ua)
, mobile = !tablet && /[^-]mobi/i.test(ua)
, result
if (/opera|opr/i.test(ua)) {
result = {
name: 'Opera'
, opera: t
, version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
}
}
else if (/yabrowser/i.test(ua)) {
result = {
name: 'Yandex Browser'
, yandexbrowser: t
, version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
}
}
else if (windowsphone) {
result = {
name: 'Windows Phone'
, windowsphone: t
}
if (edgeVersion) {
result.msedge = t
result.version = edgeVersion
}
else {
result.msie = t
result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i)
}
}
else if (/msie|trident/i.test(ua)) {
result = {
name: 'Internet Explorer'
, msie: t
, version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
}
} else if (chromeos) {
result = {
name: 'Chrome'
, chromeos: t
, chromeBook: t
, chrome: t
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
}
} else if (/chrome.+? edge/i.test(ua)) {
result = {
name: 'Microsoft Edge'
, msedge: t
, version: edgeVersion
}
}
else if (/chrome|crios|crmo/i.test(ua)) {
result = {
name: 'Chrome'
, chrome: t
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
}
}
else if (iosdevice) {
result = {
name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
}
// WTF: version is not part of user agent in web apps
if (versionIdentifier) {
result.version = versionIdentifier
}
}
else if (sailfish) {
result = {
name: 'Sailfish'
, sailfish: t
, version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
}
}
else if (/seamonkey\//i.test(ua)) {
result = {
name: 'SeaMonkey'
, seamonkey: t
, version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
}
}
else if (/firefox|iceweasel/i.test(ua)) {
result = {
name: 'Firefox'
, firefox: t
, version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
}
if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
result.firefoxos = t
}
}
else if (silk) {
result = {
name: 'Amazon Silk'
, silk: t
, version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
}
}
else if (android) {
result = {
name: 'Android'
, version: versionIdentifier
}
}
else if (/phantom/i.test(ua)) {
result = {
name: 'PhantomJS'
, phantom: t
, version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
}
}
else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
result = {
name: 'BlackBerry'
, blackberry: t
, version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
}
}
else if (webos) {
result = {
name: 'WebOS'
, webos: t
, version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
};
/touchpad\//i.test(ua) && (result.touchpad = t)
}
else if (/bada/i.test(ua)) {
result = {
name: 'Bada'
, bada: t
, version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
};
}
else if (tizen) {
result = {
name: 'Tizen'
, tizen: t
, version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
};
}
else if (/safari/i.test(ua)) {
result = {
name: 'Safari'
, safari: t
, version: versionIdentifier
}
}
else {
result = {
name: getFirstMatch(/^(.*)\/(.*) /),
version: getSecondMatch(/^(.*)\/(.*) /)
};
}
// set webkit or gecko flag for browsers based on these engines
if (!result.msedge && /(apple)?webkit/i.test(ua)) {
result.name = result.name || "Webkit"
result.webkit = t
if (!result.version && versionIdentifier) {
result.version = versionIdentifier
}
} else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko"
result.gecko = t
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
}
// set OS flags for platforms that have multiple browsers
if (!result.msedge && (android || result.silk)) {
result.android = t
} else if (iosdevice) {
result[iosdevice] = t
result.ios = t
} else if (windows) {
result.windows = t
} else if (mac) {
result.mac = t
} else if (linux) {
result.linux = t
}
// OS version extraction
var osVersion = '';
if (result.windowsphone) {
osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
} else if (iosdevice) {
osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
osVersion = osVersion.replace(/[_\s]/g, '.');
} else if (android) {
osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
} else if (result.webos) {
osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
} else if (result.blackberry) {
osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
} else if (result.bada) {
osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
} else if (result.tizen) {
osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
}
if (osVersion) {
result.osversion = osVersion;
}
// device type extraction
var osMajorVersion = osVersion.split('.')[0];
if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
result.tablet = t
} else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
result.mobile = t
}
// Graded Browser Support
// http://developer.yahoo.com/yui/articles/gbs
if (result.msedge ||
(result.msie && result.version >= 10) ||
(result.yandexbrowser && result.version >= 15) ||
(result.chrome && result.version >= 20) ||
(result.firefox && result.version >= 20.0) ||
(result.safari && result.version >= 6) ||
(result.opera && result.version >= 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
(result.blackberry && result.version >= 10.1)
) {
result.a = t;
}
else if ((result.msie && result.version < 10) ||
(result.chrome && result.version < 20) ||
(result.firefox && result.version < 20.0) ||
(result.safari && result.version < 6) ||
(result.opera && result.version < 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] < 6)
) {
result.c = t
} else result.x = t
return result
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
bowser.test = function (browserList) {
for (var i = 0; i < browserList.length; ++i) {
var browserItem = browserList[i];
if (typeof browserItem=== 'string') {
if (browserItem in bowser) {
return true;
}
}
}
return false;
}
/*
* Set our detect method to the main bowser object so we can
* reuse it to test other user agents.
* This is needed to implement future tests.
*/
bowser._detect = detect;
return bowser
});

216
src/static/js/vendors/browser.ts vendored Normal file
View file

@ -0,0 +1,216 @@
// WARNING: This file may have been modified from original.
// TODO: Check requirement of this file, this afaik was to cover weird edge cases
// that have probably been fixed in browsers.
/*!
* Bowser - a browser detector
* https://github.com/ded/bowser
* MIT License | (c) Dustin Diaz 2015
*/
export class BrowserDetector {
webos?: boolean
name: string = ''
opera?: boolean
version?: string
yandexbrowser?: boolean
windowsphone?: boolean
msedge?: boolean
msie?: boolean
chromeos?: boolean
chromeBook?: boolean
chrome?: boolean
sailfish?: boolean
seamonkey?: boolean
firefox?: boolean
firefoxos?: boolean
silk?: boolean
phantom?: boolean
blackberry?: boolean
touchpad?: boolean
bada?: boolean
tizen?: boolean
safari?: boolean
webkit?: boolean
gecko?: boolean
android?: boolean
ios?: boolean
windows?: boolean
mac?: boolean
linux?: boolean
osversion?: string
tablet?: boolean
mobile?: boolean
a?: boolean
c?: boolean
x?: boolean
touchepad?: boolean
constructor() {
this.detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
}
private getFirstMatch = (regex: RegExp, ua:string)=> {
const match = ua.match(regex);
return (match && match.length > 1 && match[1]) || '';
}
public detect = (ua: string)=>{
let iosdevice = this.getFirstMatch(/(ipod|iphone|ipad)/i, ua).toLowerCase()
let likeAndroid = /like android/i.test(ua)
let android = !likeAndroid && /android/i.test(ua)
let chromeos = /CrOS/.test(ua)
, silk = /silk/i.test(ua)
, sailfish = /sailfish/i.test(ua)
, tizen = /tizen/i.test(ua)
, webos = /(web|hpw)os/i.test(ua)
, windowsphone = /windows phone/i.test(ua)
, windows = !windowsphone && /windows/i.test(ua)
, mac = !iosdevice && !silk && /macintosh/i.test(ua)
, linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua)
, edgeVersion = this.getFirstMatch(/edge\/(\d+(\.\d+)?)/i, ua)
, versionIdentifier = this.getFirstMatch(/version\/(\d+(\.\d+)?)/i, ua)
, tablet = /tablet/i.test(ua)
, mobile = !tablet && /[^-]mobi/i.test(ua)
if (/opera|opr/i.test(ua)) {
this.name = 'Opera'
this.opera = true
this.version = versionIdentifier || this.getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i, ua)
}
else if (/yabrowser/i.test(ua)) {
this.name = 'Yandex Browser'
this.yandexbrowser = true
this.version = versionIdentifier || this.getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i, ua)
}
else if (windowsphone) {
this.name = 'Windows Phone'
this.windowsphone = true
if (edgeVersion) {
this.msedge = true
this.version = edgeVersion
}
else {
this.msie = true
this.version = this.getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i, ua)
}
}
else if (/msie|trident/i.test(ua)) {
this.name = 'Internet Explorer'
this.msie = true
this.version = this.getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i, ua)
} else if (chromeos) {
this.name = 'Chrome';
this.chromeos = true;
this.chromeBook = true;
this.chrome = true;
this.version = this.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i, ua);
} else if (/chrome.+? edge/i.test(ua)) {
this.name = 'Microsoft Edge';
this.msedge = true;
this.version = edgeVersion;
} else if (/chrome|crios|crmo/i.test(ua)) {
this.name = 'Chrome';
this.chrome = true;
this.version = this.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i, ua);
} else if (iosdevice) {
this.name = iosdevice === 'iphone' ? 'iPhone' : iosdevice === 'ipad' ? 'iPad' : 'iPod';
if (versionIdentifier) {
this.version = versionIdentifier;
}
} else if (webos) {
this.name = 'WebOS';
this.webos = true;
this.version = versionIdentifier || this.getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i, ua);
/touchpad\//i.test(ua) && (this.touchepad = true);
} else if (android) {
this.name = 'Android';
this.version = versionIdentifier;
} else if (/safari/i.test(ua)) {
this.name = 'Safari';
this.safari = true;
this.version = versionIdentifier;
} else {
this.name = this.getFirstMatch(/^(.*)\/(.*) /, ua);
this.version = this.getSecondMatch(/^(.*)\/(.*) /, ua);
}
if (!this.msedge && /(apple)?webkit/i.test(ua)) {
this.name = this.name || "Webkit";
this.webkit = true;
if (!this.version && versionIdentifier) {
this.version = versionIdentifier;
}
} else if (/gecko\//i.test(ua) && !this.webkit && !this.msedge) {
this.name = this.name || "Gecko";
this.gecko = true;
this.version = this.version || this.getFirstMatch(/gecko\/(\d+(\.\d+)?)/i, ua);
}
if (!this.msedge && (android || this.silk)) {
this.android = true;
} else if (iosdevice) {
// @ts-ignore
this[iosdevice] = true;
this.ios = true;
} else if (windows) {
this.windows = true;
} else if (mac) {
this.mac = true;
} else if (linux) {
this.linux = true;
}
let osVersion = '';
if (iosdevice) {
osVersion = this.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, ua).replace(/[_\s]/g, '.');
} else if (android) {
osVersion = this.getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i, ua);
} else if (this.webos) {
osVersion = this.getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i, ua);
}
osVersion && (this.osversion = osVersion);
if (tablet || iosdevice === 'ipad' || (android && (osVersion.split('.')[0] === '3' || osVersion.split('.')[0] === '4' && !mobile)) || this.silk) {
this.tablet = true;
} else if (mobile || iosdevice === 'iphone' || iosdevice === 'ipod' || android) {
this.mobile = true;
}
if (this.msedge ||
(this.chrome && this.version && parseInt(this.version) >= 20) ||
(this.firefox && this.version && parseInt(this.version) >= 20) ||
(this.safari && this.version && parseInt(this.version) >= 6) ||
(this.opera && this.version && parseInt(this.version) >= 10) ||
(this.ios && this.osversion && parseInt(this.osversion.split(".")[0]) >= 6)
) {
this.a = true;
} else if ((this.chrome && this.version && parseInt(this.version) < 20) ||
(this.firefox && this.version && parseInt(this.version) < 20) ||
(this.safari && this.version && parseInt(this.version) < 6)
) {
this.c = true;
} else {
this.x = true;
}
}
private getSecondMatch = (regex: RegExp, ua: string) => {
const match = ua.match(regex);
return (match && match.length > 1 && match[2]) || '';
}
test = (browserList: string)=> {
for (let i = 0; i < browserList.length; ++i) {
const browserItem = browserList[i];
if (typeof browserItem=== 'string') {
if (browserItem in this) {
return true;
}
}
}
return false;
}
}

View file

@ -343,7 +343,6 @@
$('#editorcontainerbox').append(this._tpl_wrap_bottom); $('#editorcontainerbox').append(this._tpl_wrap_bottom);
} }
} }
} }
})(jQuery); })(jQuery);

View file

@ -14,7 +14,7 @@
const basePath = new URL('..', window.location.href).pathname; const basePath = new URL('..', window.location.href).pathname;
window.$ = window.jQuery = require('../../src/static/js/vendors/jquery'); window.$ = window.jQuery = require('../../src/static/js/vendors/jquery');
window.browser = require('../../src/static/js/vendors/browser'); window.browser = require('../static/js/vendors/browser');
const pad = require('../../src/static/js/pad'); const pad = require('../../src/static/js/pad');
pad.baseURL = basePath; pad.baseURL = basePath;
window.plugins = require('../../src/static/js/pluginfw/client_plugins'); window.plugins = require('../../src/static/js/pluginfw/client_plugins');
@ -23,8 +23,8 @@
// TODO: These globals shouldn't exist. // TODO: These globals shouldn't exist.
window.pad = pad.pad; window.pad = pad.pad;
window.chat = require('../../src/static/js/chat').chat; window.chat = require('../../src/static/js/chat').chat;
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar; window.padeditbar = require('../static/js/pad_editbar').padeditbar;
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp; window.padimpexp = require('../static/js/pad_impexp').padimpexp;
await import('../../src/static/js/skin_variants') await import('../../src/static/js/skin_variants')
await import('../../src/static/js/basic_error_handler') await import('../../src/static/js/basic_error_handler')

View file

@ -19,7 +19,7 @@ import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/vendors/jquery'); // Expose jQuery #HACK window.$ = window.jQuery = require('ep_etherpad-lite/static/js/vendors/jquery'); // Expose jQuery #HACK
require('ep_etherpad-lite/static/js/vendors/gritter') require('ep_etherpad-lite/static/js/vendors/gritter')
window.browser = require('ep_etherpad-lite/static/js/vendors/browser'); window.browser = require('src/static/js/vendors/browser');
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
const socket = timeSlider.socket; const socket = timeSlider.socket;
@ -31,8 +31,8 @@ import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
/* TODO: These globals shouldn't exist. */ /* TODO: These globals shouldn't exist. */
}); });
const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; const padeditbar = require('src/static/js/pad_editbar').padeditbar;
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; const padimpexp = require('src/static/js/pad_impexp').padimpexp;
setBaseURl(baseURL) setBaseURl(baseURL)
timeSlider.init(); timeSlider.init();
padeditbar.init() padeditbar.init()

View file

@ -3,7 +3,7 @@
import {MapArrayType} from "../../../node/types/MapType"; import {MapArrayType} from "../../../node/types/MapType";
import {PluginDef} from "../../../node/types/PartType"; import {PluginDef} from "../../../node/types/PartType";
const ChatMessage = require('../../../static/js/ChatMessage'); import ChatMessage from '../../../static/js/ChatMessage';
const {Pad} = require('../../../node/db/Pad'); const {Pad} = require('../../../node/db/Pad');
const assert = require('assert').strict; const assert = require('assert').strict;
const common = require('../common'); const common = require('../common');
@ -13,7 +13,7 @@ const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
const logger = common.logger; const logger = common.logger;
type CheckFN = ({message, pad, padId}:{ type CheckFN = ({message, pad, padId}:{
message?: typeof ChatMessage, message?: ChatMessage,
pad?: typeof Pad, pad?: typeof Pad,
padId?: string, padId?: string,
})=>void; })=>void;
@ -103,10 +103,10 @@ describe(__filename, function () {
checkHook('chatNewMessage', ({message}) => { checkHook('chatNewMessage', ({message}) => {
assert(message != null); assert(message != null);
assert(message instanceof ChatMessage); assert(message instanceof ChatMessage);
assert.equal(message.authorId, authorId); assert.equal(message!.authorId, authorId);
assert.equal(message.text, this.test!.title); assert.equal(message!.text, this.test!.title);
assert(message.time >= start); assert(message!.time! >= start);
assert(message.time <= Date.now()); assert(message!.time! <= Date.now());
}), }),
sendChat(socket, {text: this.test!.title}), sendChat(socket, {text: this.test!.title}),
]); ]);
@ -153,8 +153,8 @@ describe(__filename, function () {
const customMetadata = {foo: this.test!.title}; const customMetadata = {foo: this.test!.title};
await Promise.all([ await Promise.all([
checkHook('chatNewMessage', ({message}) => { checkHook('chatNewMessage', ({message}) => {
message.text = modifiedText; message!.text = modifiedText;
message.customMetadata = customMetadata; message!.customMetadata = customMetadata;
}), }),
(async () => { (async () => {
const {message} = await listen('CHAT_MESSAGE'); const {message} = await listen('CHAT_MESSAGE');

View file

@ -16,7 +16,7 @@
<script src="../../static/js/vendors/jquery.js"></script> <script src="../../static/js/vendors/jquery.js"></script>
<script src="lib/sendkeys.js"></script> <script src="lib/sendkeys.js"></script>
<script src="../../static/js/vendors/browser.js"></script> <script src="../../static/js/vendors/browser.ts"></script>
<script src="../../static/plugins/js-cookie/dist/js.cookie.js"></script> <script src="../../static/plugins/js-cookie/dist/js.cookie.js"></script>
<script src="lib/underscore.js"></script> <script src="lib/underscore.js"></script>