mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Added more frontend ts files
This commit is contained in:
parent
cef2af15b9
commit
fa2d6d15a9
37 changed files with 2871 additions and 2534 deletions
|
@ -324,6 +324,9 @@ importers:
|
|||
'@types/underscore':
|
||||
specifier: ^1.11.15
|
||||
version: 1.11.15
|
||||
'@types/unorm':
|
||||
specifier: ^1.3.31
|
||||
version: 1.3.31
|
||||
chokidar:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
|
@ -1612,6 +1615,9 @@ packages:
|
|||
'@types/unist@3.0.2':
|
||||
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':
|
||||
resolution: {integrity: sha512-3l1qMm3wqO0iyC5gkADzT95UVW7C/XXcdvUcShOideKF0ddgVRErEQQJXBd2kvQm+aSgqhBGHGB38TgMeT57Ww==}
|
||||
|
||||
|
@ -5603,6 +5609,8 @@ snapshots:
|
|||
|
||||
'@types/unist@3.0.2': {}
|
||||
|
||||
'@types/unorm@1.3.31': {}
|
||||
|
||||
'@types/url-join@4.0.3': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.20': {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {MapArrayType} from "./MapType";
|
||||
import {PadOption} from "../../static/js/types/SocketIOMessage";
|
||||
|
||||
export type PadType = {
|
||||
id: string,
|
||||
|
@ -19,7 +20,7 @@ export type PadType = {
|
|||
getRevisionDate: (rev: number)=>Promise<number>,
|
||||
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,
|
||||
appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>,
|
||||
settings:any
|
||||
settings: PadOption
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
"@types/sinon": "^17.0.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/underscore": "^1.11.15",
|
||||
"@types/unorm": "^1.3.31",
|
||||
"chokidar": "^3.6.0",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-etherpad": "^4.0.4",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
const attributes = require('./attributes');
|
||||
|
||||
|
@ -66,7 +67,7 @@ class AttributeMap extends Map {
|
|||
* key is removed from this map (if present).
|
||||
* @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) {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
|
|
|
@ -1107,7 +1107,7 @@ export const attribsAttributeValue = (attribs: string, key: string, pool: Attrib
|
|||
* ignored if `attribs` is an attribute string.
|
||||
* @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(
|
||||
'Changeset.makeAttribsString() is deprecated; ' +
|
||||
'use AttributeMap.prototype.toString() or attributes.attribsToString() instead');
|
||||
|
|
|
@ -9,11 +9,11 @@ import {padUtils} from './pad_utils'
|
|||
* Supports serialization to JSON.
|
||||
*/
|
||||
class ChatMessage {
|
||||
|
||||
private text: string|null
|
||||
private authorId: string|null
|
||||
customMetadata: any
|
||||
text: string|null
|
||||
public authorId: string|null
|
||||
private displayName: string|null
|
||||
private time: number|null
|
||||
time: number|null
|
||||
static fromObject(obj: ChatMessage) {
|
||||
// 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.
|
||||
|
@ -108,4 +108,4 @@ class ChatMessage {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = ChatMessage;
|
||||
export default ChatMessage
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import {InnerWindow} from "./types/InnerWindow";
|
||||
import {AText} from "./types/AText";
|
||||
import AttributePool from "./AttributePool";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
|
@ -30,7 +32,8 @@ const hooks = require('./pluginfw/hooks');
|
|||
|
||||
const pluginUtils = require('./pluginfw/shared');
|
||||
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 {Cssmanager} = require("./cssmanager");
|
||||
// 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);
|
||||
};
|
||||
cleanup = () => {
|
||||
cleanup = () => {};
|
||||
cleanup = () => {
|
||||
};
|
||||
obj!.removeEventListener(event, successCb);
|
||||
obj!.removeEventListener('error', errorCb);
|
||||
};
|
||||
|
@ -90,6 +94,26 @@ const frameReady = async (frame: HTMLIFrameElement) => {
|
|||
};
|
||||
|
||||
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};
|
||||
loaded = false;
|
||||
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);
|
||||
if (this.loaded) return action();
|
||||
this.actionsPendingInit.push(action);
|
||||
|
@ -176,7 +200,7 @@ export class Ace2Editor {
|
|||
this.info = null; // prevent IE 6 closure memory leaks
|
||||
});
|
||||
|
||||
init = async (containerId: string, initialCode: string)=> {
|
||||
init = async (containerId: string, initialCode: string) => {
|
||||
debugLog('Ace2Editor.init()');
|
||||
// @ts-ignore
|
||||
this.importText(initialCode);
|
||||
|
@ -296,7 +320,7 @@ export class Ace2Editor {
|
|||
await innerWindow.Ace2Inner.init(this.info, {
|
||||
inner: new Cssmanager(innerStyle.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');
|
||||
this.loaded = true;
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {MapArrayType} from "../../node/types/MapType";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -22,11 +24,13 @@
|
|||
* 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
|
||||
// copied to new nodes that are spawned during editing; also,
|
||||
// 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.
|
||||
|
||||
|
||||
const binarySearch = (numItems, func) => {
|
||||
export const binarySearch = (numItems: number, func: (num: number)=>boolean) => {
|
||||
if (numItems < 1) return 0;
|
||||
if (func(0)) return 0;
|
||||
if (!func(numItems - 1)) return numItems;
|
||||
|
@ -52,17 +56,10 @@ const binarySearch = (numItems, func) => {
|
|||
return high;
|
||||
};
|
||||
|
||||
const binarySearchInfinite = (expectedLength, func) => {
|
||||
export const binarySearchInfinite = (expectedLength: number, func: (num: number)=>boolean) => {
|
||||
let i = 0;
|
||||
while (!func(i)) i += expectedLength;
|
||||
return binarySearch(i, func);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
exports.isNodeText = isNodeText;
|
||||
exports.getAssoc = getAssoc;
|
||||
exports.setAssoc = setAssoc;
|
||||
exports.binarySearch = binarySearch;
|
||||
exports.binarySearchInfinite = binarySearchInfinite;
|
||||
exports.noop = noop;
|
||||
export const noop = () => {};
|
|
@ -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;
|
524
src/static/js/collab_client.ts
Normal file
524
src/static/js/collab_client.ts
Normal 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
|
|
@ -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
113
src/static/js/colorutils.ts
Normal 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
|
|
@ -8,6 +8,8 @@
|
|||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
import AttributePool from "./AttributePool";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -27,12 +29,22 @@
|
|||
const _MAX_LIST_LEVEL = 16;
|
||||
|
||||
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 hooks = require('./pluginfw/hooks');
|
||||
|
||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||
const tagName = (n) => n.tagName && n.tagName.toLowerCase();
|
||||
type Tag = {
|
||||
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
|
||||
const supportedElems = new Set([
|
||||
'author',
|
||||
|
@ -56,131 +68,178 @@ const supportedElems = new Set([
|
|||
'ul',
|
||||
]);
|
||||
|
||||
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
|
||||
const _blockElems = {
|
||||
div: 1,
|
||||
p: 1,
|
||||
pre: 1,
|
||||
li: 1,
|
||||
};
|
||||
type ContentElem = Element & {
|
||||
name?: string
|
||||
}
|
||||
|
||||
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
||||
_blockElems[element] = 1;
|
||||
supportedElems.add(element);
|
||||
});
|
||||
class Lines {
|
||||
private textArray: string[] = [];
|
||||
private attribsArray: string[] = [];
|
||||
private attribsBuilder:SmartOpAssembler|null = null;
|
||||
private op = new Changeset.Op('+');
|
||||
|
||||
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
|
||||
|
||||
const textify = (str) => sanitizeUnicode(
|
||||
str.replace(/(\n | \n)/g, ' ')
|
||||
.replace(/[\n\r ]/g, ' ')
|
||||
.replace(/\xa0/g, ' ')
|
||||
.replace(/\t/g, ' '));
|
||||
length= () => this.textArray.length
|
||||
atColumnZero = () => this.textArray[this.textArray.length - 1] === ''
|
||||
startNew= () => {
|
||||
this.textArray.push('');
|
||||
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 = (() => {
|
||||
const textArray = [];
|
||||
const attribsArray = [];
|
||||
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 = {};
|
||||
type ContentCollectorPoint = {
|
||||
index: number;
|
||||
node: Node
|
||||
}
|
||||
|
||||
const _ensureColumnZero = (state) => {
|
||||
if (!lines.atColumnZero()) {
|
||||
cc.startNewLine(state);
|
||||
type ContentCollectorSel = {
|
||||
startPoint: ContentCollectorPoint
|
||||
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,
|
||||
}
|
||||
};
|
||||
let selection, startPoint, endPoint;
|
||||
let selStart = [-1, -1];
|
||||
let selEnd = [-1, -1];
|
||||
const _isEmpty = (node, state) => {
|
||||
this.abrowser = abrowser
|
||||
this.collectStyles = collectStyles
|
||||
this.apool = apool
|
||||
this.className2Author = className2Author
|
||||
|
||||
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
|
||||
if (node.childNodes.length === 0) return true;
|
||||
if (node.childNodes.length === 1 &&
|
||||
getAssoc(node, 'shouldBeEmpty') &&
|
||||
node.innerHTML === ' ' &&
|
||||
!getAssoc(node, 'unpasted')) {
|
||||
// @ts-ignore
|
||||
this.getAssoc(node, 'shouldBeEmpty') &&
|
||||
node.innerHTML === ' ' &&
|
||||
// @ts-ignore
|
||||
!this.getAssoc(node, 'unpasted')) {
|
||||
if (state) {
|
||||
const child = node.childNodes[0];
|
||||
_reachPoint(child, 0, state);
|
||||
_reachPoint(child, 1, state);
|
||||
this.reachPoint(child, 0, state);
|
||||
this.reachPoint(child, 1, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const _pointHere = (charsAfter, state) => {
|
||||
const ln = lines.length() - 1;
|
||||
let chr = lines.textOfLine(ln).length;
|
||||
}
|
||||
pointHere = (charsAfter: number, state: ContentCollectorState) => {
|
||||
const ln = this.lines.length() - 1;
|
||||
let chr = this.lines.textOfLine(ln).length;
|
||||
if (chr === 0 && Object.keys(state.lineAttributes).length !== 0) {
|
||||
chr += 1; // listMarker
|
||||
}
|
||||
chr += charsAfter;
|
||||
return [ln, chr];
|
||||
};
|
||||
}
|
||||
|
||||
const _reachBlockPoint = (nd, idx, state) => {
|
||||
if (nd.nodeType !== nd.TEXT_NODE) _reachPoint(nd, idx, state);
|
||||
};
|
||||
|
||||
const _reachPoint = (nd, idx, state) => {
|
||||
if (startPoint && nd === startPoint.node && startPoint.index === idx) {
|
||||
selStart = _pointHere(0, state);
|
||||
reachBlockPoint = (nd: ContentElem, idx: number, state: ContentCollectorState) => {
|
||||
if (nd.nodeType !== nd.TEXT_NODE) this.reachPoint(nd, idx, state);
|
||||
}
|
||||
reachPoint = (nd: Node, idx: number, state: ContentCollectorState) => {
|
||||
if (this.startPoint && nd === this.startPoint.node && this.startPoint.index === idx) {
|
||||
this.selStart = this.pointHere(0, state);
|
||||
}
|
||||
if (endPoint && nd === endPoint.node && endPoint.index === idx) {
|
||||
selEnd = _pointHere(0, state);
|
||||
if (this.endPoint && nd === this.endPoint.node && this.endPoint.index === idx) {
|
||||
this.selEnd = this.pointHere(0, state);
|
||||
}
|
||||
};
|
||||
cc.incrementFlag = (state, flagName) => {
|
||||
}
|
||||
incrementFlag = (state: ContentCollectorState, flagName: string) => {
|
||||
state.flags[flagName] = (state.flags[flagName] || 0) + 1;
|
||||
};
|
||||
cc.decrementFlag = (state, flagName) => {
|
||||
}
|
||||
decrementFlag = (state: ContentCollectorState, flagName: string) => {
|
||||
state.flags[flagName]--;
|
||||
};
|
||||
cc.incrementAttrib = (state, attribName) => {
|
||||
}
|
||||
incrementAttrib = (state: ContentCollectorState, attribName: string) => {
|
||||
if (!state.attribs[attribName]) {
|
||||
state.attribs[attribName] = 1;
|
||||
} else {
|
||||
state.attribs[attribName]++;
|
||||
}
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
cc.decrementAttrib = (state, attribName) => {
|
||||
this.recalcAttribString(state);
|
||||
}
|
||||
decrementAttrib = (state: ContentCollectorState, attribName: string) => {
|
||||
state.attribs[attribName]--;
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
|
||||
const _enterList = (state, listType) => {
|
||||
this.recalcAttribString(state);
|
||||
}
|
||||
private enterList = (state: ContentCollectorState, listType?: string) => {
|
||||
if (!listType) return;
|
||||
const oldListType = state.lineAttributes.list;
|
||||
if (listType !== 'none') {
|
||||
|
@ -196,13 +255,13 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
} else {
|
||||
state.lineAttributes.list = listType;
|
||||
}
|
||||
_recalcAttribString(state);
|
||||
this.recalcAttribString(state);
|
||||
return oldListType;
|
||||
};
|
||||
}
|
||||
|
||||
const _exitList = (state, oldListType) => {
|
||||
private exitList = (state: ContentCollectorState, oldListType: string) => {
|
||||
if (state.lineAttributes.list) {
|
||||
state.listNesting--;
|
||||
state.listNesting!--;
|
||||
}
|
||||
if (oldListType && oldListType !== 'none') {
|
||||
state.lineAttributes.list = oldListType;
|
||||
|
@ -210,25 +269,22 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
delete state.lineAttributes.list;
|
||||
delete state.lineAttributes.start;
|
||||
}
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
|
||||
const _enterAuthor = (state, author) => {
|
||||
this.recalcAttribString(state);
|
||||
}
|
||||
private enterAuthor = (state: ContentCollectorState, author: string) => {
|
||||
const oldAuthor = state.author;
|
||||
state.authorLevel = (state.authorLevel || 0) + 1;
|
||||
state.author = author;
|
||||
_recalcAttribString(state);
|
||||
this.recalcAttribString(state);
|
||||
return oldAuthor;
|
||||
};
|
||||
|
||||
const _exitAuthor = (state, oldAuthor) => {
|
||||
state.authorLevel--;
|
||||
}
|
||||
private exitAuthor = (state: ContentCollectorState, oldAuthor: string) => {
|
||||
state.authorLevel!--;
|
||||
state.author = oldAuthor;
|
||||
_recalcAttribString(state);
|
||||
};
|
||||
|
||||
const _recalcAttribString = (state) => {
|
||||
const attribs = new AttributeMap(apool);
|
||||
this.recalcAttribString(state);
|
||||
}
|
||||
private recalcAttribString = (state: ContentCollectorState) => {
|
||||
const attribs = new AttributeMap(this.apool);
|
||||
for (const [a, count] of Object.entries(state.attribs)) {
|
||||
if (!count) continue;
|
||||
// The following splitting of the attribute name is a workaround
|
||||
|
@ -253,49 +309,50 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
attribs.set(a, 'true');
|
||||
}
|
||||
}
|
||||
if (state.authorLevel > 0) {
|
||||
if (apool.putAttrib(['author', state.author], true) >= 0) {
|
||||
if (state.authorLevel! > 0) {
|
||||
if (this.apool!.putAttrib(['author', state.author!], true) >= 0) {
|
||||
// require that author already be in pool
|
||||
// (don't add authors from other documents, etc.)
|
||||
if (state.author) attribs.set('author', state.author);
|
||||
}
|
||||
}
|
||||
state.attribString = attribs.toString();
|
||||
};
|
||||
|
||||
const _produceLineAttributesMarker = (state) => {
|
||||
}
|
||||
private produceLineAttributesMarker = (state: ContentCollectorState) => {
|
||||
// TODO: This has to go to AttributeManager.
|
||||
const attribs = new AttributeMap(apool)
|
||||
.set('lmkr', '1')
|
||||
.set('insertorder', 'first')
|
||||
// TODO: Converting all falsy values in state.lineAttributes into removals is awkward.
|
||||
// Better would be to never add 0, false, null, or undefined to state.lineAttributes in the
|
||||
// first place (I'm looking at you, state.lineAttributes.start).
|
||||
.update(Object.entries(state.lineAttributes).map(([k, v]) => [k, v || '']), true);
|
||||
lines.appendText('*', attribs.toString());
|
||||
};
|
||||
cc.startNewLine = (state) => {
|
||||
const attribsF = Object.entries(state.lineAttributes).map(([k, v]) => [k, v || '']) as Attribute[]
|
||||
const attribs = new AttributeMap(this.apool)
|
||||
.set('lmkr', '1')
|
||||
.set('insertorder', 'first')
|
||||
// TODO: Converting all falsy values in state.lineAttributes into removals is awkward.
|
||||
// Better would be to never add 0, false, null, or undefined to state.lineAttributes in the
|
||||
// first place (I'm looking at you, state.lineAttributes.start).
|
||||
.update(attribsF, true);
|
||||
this.lines.appendText('*', attribs.toString());
|
||||
}
|
||||
startNewLine = (state: ContentCollectorState|null) => {
|
||||
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) {
|
||||
_produceLineAttributesMarker(state);
|
||||
this.produceLineAttributesMarker(state);
|
||||
}
|
||||
}
|
||||
lines.startNew();
|
||||
};
|
||||
cc.notifySelection = (sel) => {
|
||||
this.lines.startNew();
|
||||
}
|
||||
notifySelection = (sel: ContentCollectorSel) => {
|
||||
if (sel) {
|
||||
selection = sel;
|
||||
startPoint = selection.startPoint;
|
||||
endPoint = selection.endPoint;
|
||||
this.selection = sel;
|
||||
this.startPoint = this.selection.startPoint;
|
||||
this.endPoint = this.selection.endPoint;
|
||||
}
|
||||
};
|
||||
cc.doAttrib = (state, na) => {
|
||||
}
|
||||
doAttrib = (state: ContentCollectorState, na: string) => {
|
||||
state.localAttribs = (state.localAttribs || []);
|
||||
state.localAttribs.push(na);
|
||||
cc.incrementAttrib(state, na);
|
||||
};
|
||||
cc.collectContent = function (node, state) {
|
||||
this.incrementAttrib(state, na);
|
||||
}
|
||||
|
||||
collectContent = (node: ContentElem, state: ContentCollectorState)=> {
|
||||
let unsupportedElements = null;
|
||||
if (!state) {
|
||||
state = {
|
||||
|
@ -318,33 +375,33 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
}
|
||||
const localAttribs = state.localAttribs;
|
||||
state.localAttribs = null;
|
||||
const isBlock = isBlockElement(node);
|
||||
const isBlock = this.isBlockElement(node);
|
||||
if (!isBlock && node.name && (node.name !== 'body')) {
|
||||
if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name);
|
||||
}
|
||||
const isEmpty = _isEmpty(node, state);
|
||||
if (isBlock) _ensureColumnZero(state);
|
||||
const startLine = lines.length() - 1;
|
||||
_reachBlockPoint(node, 0, state);
|
||||
const isEmpty = this.isEmpty(node, state);
|
||||
if (isBlock) this.ensureColumnZero(state);
|
||||
const startLine = this.lines.length() - 1;
|
||||
this.reachBlockPoint(node, 0, state);
|
||||
|
||||
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};
|
||||
// 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
|
||||
// modify context.text, the first hook function to return a string wins.
|
||||
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 rest = '';
|
||||
let x = 0; // offset into original text
|
||||
if (txt.length === 0) {
|
||||
if (startPoint && node === startPoint.node) {
|
||||
selStart = _pointHere(0, state);
|
||||
if (this.startPoint && node === this.startPoint.node) {
|
||||
this.selStart = this.pointHere(0, state);
|
||||
}
|
||||
if (endPoint && node === endPoint.node) {
|
||||
selEnd = _pointHere(0, state);
|
||||
if (this.endPoint && node === this.endPoint.node) {
|
||||
this.selEnd = this.pointHere(0, state);
|
||||
}
|
||||
}
|
||||
while (txt.length > 0) {
|
||||
|
@ -356,11 +413,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
txt = firstLine;
|
||||
} else { /* will only run this loop body once */
|
||||
}
|
||||
if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) {
|
||||
selStart = _pointHere(startPoint.index - x, state);
|
||||
if (this.startPoint && node === this.startPoint.node && this.startPoint.index - x <= txt.length) {
|
||||
this.selStart = this.pointHere(this.startPoint.index - x, state);
|
||||
}
|
||||
if (endPoint && node === endPoint.node && endPoint.index - x <= txt.length) {
|
||||
selEnd = _pointHere(endPoint.index - x, state);
|
||||
if (this.endPoint && node === this.endPoint.node && this.endPoint.index - x <= txt.length) {
|
||||
this.selEnd = this.pointHere(this.endPoint.index - x, state);
|
||||
}
|
||||
let txt2 = 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.
|
||||
txt2 = '';
|
||||
}
|
||||
const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0;
|
||||
const atBeginningOfLine = this.lines.textOfLine(this.lines.length() - 1).length === 0;
|
||||
if (atBeginningOfLine) {
|
||||
// newlines in the source mustn't become spaces at beginning of line box
|
||||
txt2 = txt2.replace(/^\n*/, '');
|
||||
}
|
||||
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;
|
||||
txt = rest;
|
||||
if (txt.length > 0) {
|
||||
cc.startNewLine(state);
|
||||
this.startNewLine(state);
|
||||
}
|
||||
}
|
||||
} else if (node.nodeType === node.ELEMENT_NODE) {
|
||||
const tname = tagName(node) || '';
|
||||
const tname = tagName(node as Element) || '';
|
||||
|
||||
if (tname === 'img') {
|
||||
hooks.callAll('collectContentImage', {
|
||||
cc,
|
||||
cc: this,
|
||||
state,
|
||||
tname,
|
||||
styl: null,
|
||||
|
@ -414,18 +471,18 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
cls: null,
|
||||
});
|
||||
if (startNewLine) {
|
||||
cc.startNewLine(state);
|
||||
this.startNewLine(state);
|
||||
}
|
||||
} else if (tname === 'script' || tname === 'style') {
|
||||
// ignore
|
||||
} else if (!isEmpty) {
|
||||
let styl = node.getAttribute('style');
|
||||
let cls = node.getAttribute('class');
|
||||
let isPre = (tname === 'pre');
|
||||
if ((!isPre) && abrowser && abrowser.safari) {
|
||||
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
|
||||
let isPre: boolean| RegExpExecArray|"" = (tname === 'pre');
|
||||
if ((!isPre) && this.abrowser && this.abrowser.safari) {
|
||||
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 oldAuthorOrNull = null;
|
||||
|
||||
|
@ -438,33 +495,33 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
// for now it shows how to fix the problem
|
||||
return;
|
||||
}
|
||||
if (collectStyles) {
|
||||
if (this.collectStyles) {
|
||||
hooks.callAll('collectContentPre', {
|
||||
cc,
|
||||
cc: this,
|
||||
state,
|
||||
tname,
|
||||
styl,
|
||||
cls,
|
||||
});
|
||||
if (tname === 'b' ||
|
||||
(styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
|
||||
tname === 'strong') {
|
||||
cc.doAttrib(state, 'bold');
|
||||
(styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
|
||||
tname === 'strong') {
|
||||
this.doAttrib(state, 'bold');
|
||||
}
|
||||
if (tname === 'i' ||
|
||||
(styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
|
||||
tname === 'em') {
|
||||
cc.doAttrib(state, 'italic');
|
||||
(styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
|
||||
tname === 'em') {
|
||||
this.doAttrib(state, 'italic');
|
||||
}
|
||||
if (tname === 'u' ||
|
||||
(styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
|
||||
tname === 'ins') {
|
||||
cc.doAttrib(state, 'underline');
|
||||
(styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
|
||||
tname === 'ins') {
|
||||
this.doAttrib(state, 'underline');
|
||||
}
|
||||
if (tname === 's' ||
|
||||
(styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
|
||||
tname === 'del') {
|
||||
cc.doAttrib(state, 'strikethrough');
|
||||
(styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
|
||||
tname === 'del') {
|
||||
this.doAttrib(state, 'strikethrough');
|
||||
}
|
||||
if (tname === 'ul' || tname === 'ol') {
|
||||
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
|
||||
if (!rr && !type) {
|
||||
for (const child of node.childNodes) {
|
||||
if (tagName(child) !== 'ul') continue;
|
||||
type = child.getAttribute('class');
|
||||
if (tagName(child as ContentElem) !== 'ul') continue;
|
||||
type = (child as ContentElem).getAttribute('class');
|
||||
if (type) break;
|
||||
}
|
||||
}
|
||||
|
@ -493,22 +550,24 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
}
|
||||
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/)) {
|
||||
// This has undesirable behavior in Chrome but is right in other browsers.
|
||||
// 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') {
|
||||
state.lineAttributes.start = state.start || 0;
|
||||
_recalcAttribString(state);
|
||||
if (state.lineAttributes.list.indexOf('number') !== -1) {
|
||||
this.recalcAttribString(state);
|
||||
if (state.lineAttributes.list!.indexOf('number') !== -1) {
|
||||
/*
|
||||
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>
|
||||
Note how the <ol> item has to be inside a <li>
|
||||
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)
|
||||
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
|
||||
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.
|
||||
if (node.parentNode && tagName(node.parentNode) === 'ul') {
|
||||
state.start++;
|
||||
if (node.parentNode && tagName(node.parentNode as Element) === 'ul') {
|
||||
state.start!++;
|
||||
// TODO, this is hacky.
|
||||
// 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
|
||||
|
@ -539,14 +598,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
// delete state.listNesting;
|
||||
// _recalcAttribString(state);
|
||||
}
|
||||
if (className2Author && cls) {
|
||||
if (this.className2Author && cls) {
|
||||
const classes = cls.match(/\S+/g);
|
||||
if (classes && classes.length > 0) {
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const c = classes[i];
|
||||
const a = className2Author(c);
|
||||
const a = this.className2Author(c);
|
||||
if (a) {
|
||||
oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
|
||||
oldAuthorOrNull = (this.enterAuthor(state, a) || 'none');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -555,12 +614,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
}
|
||||
|
||||
for (const c of node.childNodes) {
|
||||
cc.collectContent(c, state);
|
||||
this.collectContent(c as ContentElem, state);
|
||||
}
|
||||
|
||||
if (collectStyles) {
|
||||
if (this.collectStyles) {
|
||||
hooks.callAll('collectContentPost', {
|
||||
cc,
|
||||
cc: this,
|
||||
state,
|
||||
tname,
|
||||
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) {
|
||||
for (let i = 0; i < state.localAttribs.length; i++) {
|
||||
cc.decrementAttrib(state, state.localAttribs[i]);
|
||||
this.decrementAttrib(state, state.localAttribs[i]);
|
||||
}
|
||||
}
|
||||
if (oldListTypeOrNull) {
|
||||
_exitList(state, oldListTypeOrNull);
|
||||
this.exitList(state, oldListTypeOrNull);
|
||||
}
|
||||
if (oldAuthorOrNull) {
|
||||
_exitAuthor(state, oldAuthorOrNull);
|
||||
this.exitAuthor(state, oldAuthorOrNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
_reachBlockPoint(node, 1, state);
|
||||
this.reachBlockPoint(node, 1, state);
|
||||
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
|
||||
// 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.
|
||||
// commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412
|
||||
if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') {
|
||||
cc.startNewLine(state);
|
||||
this.startNewLine(state);
|
||||
}
|
||||
} else {
|
||||
_ensureColumnZero(state);
|
||||
this.ensureColumnZero(state);
|
||||
}
|
||||
}
|
||||
state.localAttribs = localAttribs;
|
||||
if (unsupportedElements && unsupportedElements.size) {
|
||||
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
|
||||
cc.notifyNextNode = (node) => {
|
||||
notifyNextNode = (node: ContentElem) => {
|
||||
// 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
|
||||
// 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
|
||||
// to end a line.
|
||||
if ((!node) || (isBlockElement(node) && !_isEmpty(node))) {
|
||||
_ensureColumnZero(null);
|
||||
if ((!node) || (this.isBlockElement(node) && !this.isEmpty(node))) {
|
||||
this.ensureColumnZero(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// each returns [line, char] or [-1,-1]
|
||||
const getSelectionStart = () => selStart;
|
||||
const getSelectionEnd = () => selEnd;
|
||||
getSelectionStart = () => this.selStart;
|
||||
getSelectionEnd = () => this.selEnd;
|
||||
|
||||
|
||||
// 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).
|
||||
// can be called at any point
|
||||
cc.getLines = () => lines.textLines();
|
||||
getLines = () => this.lines.textLines();
|
||||
|
||||
cc.finish = () => {
|
||||
lines.flush();
|
||||
const lineAttribs = lines.attribLines();
|
||||
const lineStrings = cc.getLines();
|
||||
finish = () => {
|
||||
this.lines.flush();
|
||||
const lineAttribs = this.lines.attribLines();
|
||||
const lineStrings = this.getLines();
|
||||
|
||||
lineStrings.length--;
|
||||
lineAttribs.length--;
|
||||
|
||||
const ss = getSelectionStart();
|
||||
const se = getSelectionEnd();
|
||||
const ss = this.getSelectionStart();
|
||||
const se = this.getSelectionEnd();
|
||||
|
||||
const fixLongLines = () => {
|
||||
// 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 oldAttribString = lineAttribs[i];
|
||||
if (oldString.length > lineLimit + buffer) {
|
||||
const newStrings = [];
|
||||
const newStrings: string[] = [];
|
||||
const newAttribStrings = [];
|
||||
while (oldString.length > lineLimit) {
|
||||
// var semiloc = oldString.lastIndexOf(';', lineLimit-1);
|
||||
|
@ -661,7 +722,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
newAttribStrings.push(oldAttribString);
|
||||
}
|
||||
|
||||
const fixLineNumber = (lineChar) => {
|
||||
const fixLineNumber = (lineChar: number[]) => {
|
||||
if (lineChar[0] < 0) return;
|
||||
let n = lineChar[0];
|
||||
let c = lineChar[1];
|
||||
|
@ -701,11 +762,5 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
lines: lineStrings,
|
||||
lineAttribs,
|
||||
};
|
||||
};
|
||||
|
||||
return cc;
|
||||
};
|
||||
|
||||
exports.sanitizeUnicode = sanitizeUnicode;
|
||||
exports.makeContentCollector = makeContentCollector;
|
||||
exports.supportedElems = supportedElems;
|
||||
}
|
||||
}
|
|
@ -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 += ' ';
|
||||
} 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, ' ');
|
||||
}
|
||||
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] = ' ';
|
||||
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] = ' ';
|
||||
break;
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
||||
|
||||
exports.domline = domline;
|
299
src/static/js/domline.ts
Normal file
299
src/static/js/domline.ts
Normal 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 += ' ';
|
||||
} 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, ' ');
|
||||
}
|
||||
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] = ' ';
|
||||
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] = ' ';
|
||||
break;
|
||||
} else if (p.charAt(0) !== '<') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
if (p === ' ') {
|
||||
parts[i] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// if "document" is falsy we don't create a DOM node, just
|
||||
// an object with innerHTML and className
|
||||
|
||||
export default Domline
|
|
@ -24,7 +24,7 @@ import {Socket} from "socket.io";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
let socket: null | Socket;
|
||||
let socket: null | any;
|
||||
|
||||
|
||||
// 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 chat = require('./chat').chat;
|
||||
const getCollabClient = require('./collab_client').getCollabClient;
|
||||
import Collab_client, {CollabClient} from './collab_client'
|
||||
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
||||
import padcookie from "./pad_cookie";
|
||||
|
||||
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 padmodals = require('./pad_modals').padmodals;
|
||||
const padsavedrevs = require('./pad_savedrevs');
|
||||
|
@ -52,8 +52,9 @@ import {padUtils as padutils} from "./pad_utils";
|
|||
const colorutils = require('./colorutils').colorutils;
|
||||
const randomString = require('./pad_utils').randomString;
|
||||
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 {ChangeSetLoader} from "./timeslider";
|
||||
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
|
@ -84,7 +85,7 @@ const getParameters = [
|
|||
checkVal: null,
|
||||
callback: (val: any) => {
|
||||
if (val === 'false') {
|
||||
settings.hideChat = true;
|
||||
pad.settings.hideChat = true;
|
||||
chat.hide();
|
||||
$('#chaticon').hide();
|
||||
}
|
||||
|
@ -93,58 +94,59 @@ const getParameters = [
|
|||
{
|
||||
name: 'showLineNumbers',
|
||||
checkVal: 'false',
|
||||
callback: (val) => {
|
||||
settings.LineNumbersDisabled = true;
|
||||
callback: (val: any) => {
|
||||
pad.settings.LineNumbersDisabled = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'useMonospaceFont',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
settings.useMonospaceFontGlobal = true;
|
||||
callback: (val: any) => {
|
||||
pad.settings.useMonospaceFontGlobal = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'userName',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
settings.globalUserName = val;
|
||||
callback: (val: string) => {
|
||||
pad.settings.globalUserName = val;
|
||||
window.clientVars.userName = val;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'userColor',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
settings.globalUserColor = val;
|
||||
callback: (val: number) => {
|
||||
// @ts-ignore
|
||||
pad.settings.globalUserColor = val;
|
||||
window.clientVars.userColor = val;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rtl',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
settings.rtlIsTrue = true;
|
||||
callback: (val: any) => {
|
||||
pad.settings.rtlIsTrue = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'alwaysShowChat',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
if (!settings.hideChat) chat.stickToScreen();
|
||||
callback: (val: any) => {
|
||||
if (!pad.settings.hideChat) chat.stickToScreen();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'chatAndUsers',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
callback: (val: any) => {
|
||||
chat.chatAndUsers();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lang',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
callback: (val: any) => {
|
||||
console.log('Val is', val)
|
||||
html10n.localize([val, 'en']);
|
||||
Cookies.set('language', val);
|
||||
|
@ -155,6 +157,7 @@ const getParameters = [
|
|||
const getParams = () => {
|
||||
// Tries server enforced options first..
|
||||
for (const setting of getParameters) {
|
||||
// @ts-ignore
|
||||
let value = window.clientVars.padOptions[setting.name];
|
||||
if (value == null) continue;
|
||||
value = value.toString();
|
||||
|
@ -213,7 +216,7 @@ const sendClientReady = (isReconnect: boolean) => {
|
|||
|
||||
// this is a reconnect, lets tell the server our revisionnumber
|
||||
if (isReconnect) {
|
||||
msg.client_rev = this.collabClient!.getCurrentRevisionNumber();
|
||||
msg.client_rev = pad.collabClient!.getCurrentRevisionNumber();
|
||||
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
|
||||
// server disconnect".
|
||||
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") {
|
||||
// @ts-ignore
|
||||
let date = new Date(obj.data.payload.timestamp);
|
||||
$.gritter.add({
|
||||
window.$.gritter.add({
|
||||
// (string | mandatory) the heading of the notification
|
||||
title: 'Admin message',
|
||||
// (string | mandatory) the text inside the notification
|
||||
// @ts-ignore
|
||||
text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message,
|
||||
// (bool | optional) if you want it to fade out on its own or just sit there
|
||||
// @ts-ignore
|
||||
sticky: obj.data.payload.message.sticky
|
||||
});
|
||||
}
|
||||
|
@ -282,7 +288,7 @@ const handshake = async () => {
|
|||
|
||||
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).
|
||||
if (pad.collabClient != null) {
|
||||
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.
|
||||
if (pad.collabClient != null) {
|
||||
pad.collabClient.setStateIdle();
|
||||
|
@ -303,9 +309,9 @@ const handshake = async () => {
|
|||
// 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
|
||||
if (obj.accessStatus) {
|
||||
if ("accessStatus" in obj) {
|
||||
if (obj.accessStatus === 'deny') {
|
||||
$('#loading').hide();
|
||||
$('#permissionDenied').show();
|
||||
|
@ -334,7 +340,7 @@ const handshake = async () => {
|
|||
})
|
||||
}
|
||||
|
||||
} else if (obj.disconnect) {
|
||||
} else if ("disconnect" in obj && obj.disconnect) {
|
||||
padconnectionstatus.disconnected(obj.disconnect);
|
||||
socket.disconnect();
|
||||
|
||||
|
@ -350,9 +356,9 @@ const handshake = async () => {
|
|||
});
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve) => {
|
||||
const h = (obj) => {
|
||||
if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return;
|
||||
new Promise<void>((resolve) => {
|
||||
const h = (obj: ClientVarData) => {
|
||||
if ("accessStatus" in obj || obj.type !== 'CLIENT_VARS') return;
|
||||
socket.off('message', h);
|
||||
resolve();
|
||||
};
|
||||
|
@ -368,39 +374,45 @@ const handshake = async () => {
|
|||
|
||||
/** Defers message handling until setCollabClient() is called with a non-null value. */
|
||||
class MessageQueue {
|
||||
private _q: ClientVarMessage[]
|
||||
private _cc: Collab_client | null
|
||||
constructor() {
|
||||
this._q = [];
|
||||
this._cc = null;
|
||||
}
|
||||
|
||||
setCollabClient(cc) {
|
||||
setCollabClient(cc: Collab_client) {
|
||||
this._cc = cc;
|
||||
this.enqueue(); // Flush.
|
||||
}
|
||||
|
||||
enqueue(...msgs) {
|
||||
enqueue(...msgs: ClientVarMessage[]) {
|
||||
if (this._cc == null) {
|
||||
this._q.push(...msgs);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Pad {
|
||||
private collabClient: null;
|
||||
private myUserInfo: null | {
|
||||
userId: string,
|
||||
name: string,
|
||||
ip: string,
|
||||
colorId: string,
|
||||
public collabClient: null| CollabClient;
|
||||
private myUserInfo: null | UserInfo &{
|
||||
globalUserColor?: string| boolean
|
||||
name?: string
|
||||
ip?: string
|
||||
};
|
||||
private diagnosticInfo: {
|
||||
disconnectedMessage?: string
|
||||
padId?: string
|
||||
socket?: MapArrayType<string|number>,
|
||||
collabDiagnosticInfo?: any
|
||||
};
|
||||
private diagnosticInfo: {};
|
||||
private initTime: number;
|
||||
private clientTimeOffset: null | number;
|
||||
private _messageQ: MessageQueue;
|
||||
private padOptions: MapArrayType<MapArrayType<string>>;
|
||||
_messageQ: MessageQueue;
|
||||
private padOptions: PadOption;
|
||||
settings: PadSettings = {
|
||||
LineNumbersDisabled: false,
|
||||
noColors: false,
|
||||
|
@ -409,6 +421,7 @@ export class Pad {
|
|||
globalUserColor: false,
|
||||
rtlIsTrue: false,
|
||||
}
|
||||
socket: any;
|
||||
|
||||
constructor() {
|
||||
// don't access these directly from outside this file, except
|
||||
|
@ -430,8 +443,8 @@ export class Pad {
|
|||
getUserId = () => this.myUserInfo!.userId
|
||||
getUserName = () => this.myUserInfo!.name
|
||||
userList = () => paduserlist.users()
|
||||
sendClientMessage = (msg: string) => {
|
||||
this.collabClient.sendClientMessage(msg);
|
||||
sendClientMessage = (msg: ClientSendMessages) => {
|
||||
this.collabClient!.sendClientMessage(msg);
|
||||
}
|
||||
init = () => {
|
||||
padutils.setupGlobalExceptionHandler();
|
||||
|
@ -472,7 +485,7 @@ export class Pad {
|
|||
const postAceInit = () => {
|
||||
padeditbar.init();
|
||||
setTimeout(() => {
|
||||
padeditor.ace.focus();
|
||||
padeditor.ace!.focus();
|
||||
}, 0);
|
||||
const optionsStickyChat = $('#options-stickychat');
|
||||
optionsStickyChat.on('click', () => {
|
||||
|
@ -502,14 +515,14 @@ export class Pad {
|
|||
$('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update');
|
||||
|
||||
// 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
|
||||
$('#options-chatandusers:checked').trigger('click');
|
||||
$('#options-stickychat:checked').trigger('click');
|
||||
}
|
||||
};
|
||||
const mobileMatch = window.matchMedia('(max-width: 800px)');
|
||||
mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized
|
||||
mobileMatch.addListener((ev)=>checkChatAndUsersVisibility(ev)); // check if window resized
|
||||
setTimeout(() => {
|
||||
checkChatAndUsersVisibility(mobileMatch);
|
||||
}, 0); // check now after load
|
||||
|
@ -522,21 +535,23 @@ export class Pad {
|
|||
// order of inits is important here:
|
||||
padimpexp.init(this);
|
||||
padsavedrevs.init(this);
|
||||
// @ts-ignore
|
||||
padeditor.init(this.padOptions.view || {}, this).then(postAceInit);
|
||||
paduserlist.init(this.myUserInfo, this);
|
||||
padconnectionstatus.init();
|
||||
padmodals.init(this);
|
||||
|
||||
this.collabClient = getCollabClient(
|
||||
padeditor.ace, window.clientVars.collab_client_vars, this.myUserInfo,
|
||||
this.collabClient = new CollabClient(
|
||||
padeditor.ace!, window.clientVars.collab_client_vars, this.myUserInfo!,
|
||||
{colorPalette: this.getColorPalette()}, pad);
|
||||
this._messageQ.setCollabClient(this.collabClient);
|
||||
this.collabClient.setOnUserJoin(this.handleUserJoin);
|
||||
this.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
|
||||
this.collabClient.setOnUserLeave(pad.handleUserLeave);
|
||||
this.collabClient.setOnClientMessage(pad.handleClientMessage);
|
||||
this.collabClient.setOnClientMessage(pad.handleClientMessage!);
|
||||
// @ts-ignore
|
||||
this.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
|
||||
this.collabClient.setOnInternalAction(pad.handleCollabAction);
|
||||
this.collabClient.setOnInternalAction(pad.handleCollabAction!);
|
||||
|
||||
// load initial chat-messages
|
||||
if (window.clientVars.chatHead !== -1) {
|
||||
|
@ -550,52 +565,54 @@ export class Pad {
|
|||
|
||||
if (window.clientVars.readonly) {
|
||||
chat.hide();
|
||||
// @ts-ignore
|
||||
$('#myusernameedit').attr('disabled', true);
|
||||
// @ts-ignore
|
||||
$('#chatinput').attr('disabled', true);
|
||||
$('#chaticon').hide();
|
||||
$('#options-chatandusers').parent().hide();
|
||||
$('#options-stickychat').parent().hide();
|
||||
} else if (!settings.hideChat) {
|
||||
} else if (!this.settings.hideChat) {
|
||||
$('#chaticon').show();
|
||||
}
|
||||
|
||||
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');
|
||||
|
||||
padeditor.ace.callWithAce((ace) => {
|
||||
padeditor.ace!.callWithAce((ace) => {
|
||||
ace.ace_setEditable(!window.clientVars.readonly);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// If the noColors value is set to true then we need to
|
||||
// hide the background colors on the ace spans
|
||||
if (settings.noColors === true) {
|
||||
if (this.settings.noColors === true) {
|
||||
this.changeViewOption('noColors', true);
|
||||
}
|
||||
|
||||
if (settings.rtlIsTrue === true) {
|
||||
if (this.settings.rtlIsTrue === true) {
|
||||
this.changeViewOption('rtlIsTrue', true);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
// if the globalUserName value is set we need to tell the server and
|
||||
// the client about the new authorname
|
||||
if (settings.globalUserName !== false) {
|
||||
this.notifyChangeName(settings.globalUserName); // Notifies the server
|
||||
this.myUserInfo.name = settings.globalUserName;
|
||||
$('#myusernameedit').val(settings.globalUserName); // Updates the current users UI
|
||||
if (this.settings.globalUserName !== false) {
|
||||
this.notifyChangeName(this.settings.globalUserName as string); // Notifies the server
|
||||
this.myUserInfo!.name = this.settings.globalUserName as string;
|
||||
$('#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,
|
||||
// so collabClient knows we have a query parameter.
|
||||
this.myUserInfo.globalUserColor = settings.globalUserColor;
|
||||
this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId
|
||||
this.myUserInfo!.globalUserColor = this.settings.globalUserColor!;
|
||||
this.notifyChangeColor(this.settings.globalUserColor as unknown as number); // Updates this.myUserInfo.colorId
|
||||
paduserlist.setMyUserInfo(this.myUserInfo);
|
||||
}
|
||||
}
|
||||
|
@ -603,39 +620,39 @@ export class Pad {
|
|||
dispose = () => {
|
||||
padeditor.dispose();
|
||||
}
|
||||
notifyChangeName = (newName) => {
|
||||
this.myUserInfo.name = newName;
|
||||
this.collabClient.updateUserInfo(this.myUserInfo);
|
||||
notifyChangeName = (newName: string) => {
|
||||
this.myUserInfo!.name = newName;
|
||||
this.collabClient!.updateUserInfo(this.myUserInfo!);
|
||||
}
|
||||
notifyChangeColor = (newColorId) => {
|
||||
this.myUserInfo.colorId = newColorId;
|
||||
this.collabClient.updateUserInfo(this.myUserInfo);
|
||||
notifyChangeColor = (newColorId: number) => {
|
||||
this.myUserInfo!.colorId = newColorId;
|
||||
this.collabClient!.updateUserInfo(this.myUserInfo!);
|
||||
}
|
||||
|
||||
changePadOption = (key: string, value: string) => {
|
||||
const options: MapArrayType<string> = {};
|
||||
const options: any = {}; // PadOption
|
||||
options[key] = value;
|
||||
this.handleOptionsChange(options);
|
||||
this.collabClient.sendClientMessage(
|
||||
this.collabClient!.sendClientMessage(
|
||||
{
|
||||
type: 'padoptions',
|
||||
options,
|
||||
changedBy: this.myUserInfo.name || 'unnamed',
|
||||
changedBy: this.myUserInfo!.name || 'unnamed',
|
||||
})
|
||||
}
|
||||
|
||||
changeViewOption = (key: string, value: string) => {
|
||||
const options: MapArrayType<MapArrayType<any>> =
|
||||
changeViewOption = (key: string, value: any) => {
|
||||
const options: PadOption =
|
||||
{
|
||||
view: {}
|
||||
,
|
||||
}
|
||||
;
|
||||
options.view[key] = value;
|
||||
options.view![key] = value;
|
||||
this.handleOptionsChange(options);
|
||||
}
|
||||
|
||||
handleOptionsChange = (opts: MapArrayType<MapArrayType<string>>) => {
|
||||
handleOptionsChange = (opts: PadOption) => {
|
||||
// opts object is a full set of options or just
|
||||
// some options to change
|
||||
if (opts.view) {
|
||||
|
@ -643,7 +660,9 @@ export class Pad {
|
|||
this.padOptions.view = {};
|
||||
}
|
||||
for (const [k, v] of Object.entries(opts.view)) {
|
||||
// @ts-ignore
|
||||
this.padOptions.view[k] = v;
|
||||
// @ts-ignore
|
||||
padcookie.setPref(k, v);
|
||||
}
|
||||
padeditor.setViewOptions(this.padOptions.view);
|
||||
|
@ -652,28 +671,28 @@ export class Pad {
|
|||
getPadOptions = () => this.padOptions
|
||||
suggestUserName =
|
||||
(userId: string, name: string) => {
|
||||
this.collabClient.sendClientMessage(
|
||||
this.collabClient!.sendClientMessage(
|
||||
{
|
||||
type: 'suggestUserName',
|
||||
unnamedId: userId,
|
||||
newName: name,
|
||||
});
|
||||
}
|
||||
handleUserJoin = (userInfo) => {
|
||||
handleUserJoin = (userInfo: UserInfo) => {
|
||||
paduserlist.userJoinOrUpdate(userInfo);
|
||||
}
|
||||
handleUserUpdate = (userInfo) => {
|
||||
handleUserUpdate = (userInfo: UserInfo) => {
|
||||
paduserlist.userJoinOrUpdate(userInfo);
|
||||
}
|
||||
handleUserLeave =
|
||||
(userInfo) => {
|
||||
(userInfo: UserInfo) => {
|
||||
paduserlist.userLeave(userInfo);
|
||||
}
|
||||
// caller shouldn't mutate the object
|
||||
handleClientMessage =
|
||||
(msg) => {
|
||||
(msg: ClientSendMessages) => {
|
||||
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);
|
||||
paduserlist.setMyUserInfo(pad.myUserInfo);
|
||||
}
|
||||
|
@ -689,7 +708,7 @@ export class Pad {
|
|||
|
||||
handleChannelStateChange
|
||||
=
|
||||
(newState, message) => {
|
||||
(newState: string, message: string) => {
|
||||
const oldFullyConnected = !!padconnectionstatus.isFullyConnected();
|
||||
const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting');
|
||||
if (newState === 'CONNECTED') {
|
||||
|
@ -709,10 +728,9 @@ export class Pad {
|
|||
|
||||
// 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
|
||||
for (const [i, value] of Object.entries(socket.socket || {})) {
|
||||
const type = typeof value;
|
||||
for (const [i, value] of Object.entries(socket!.socket || {})) {
|
||||
|
||||
if (type === 'string' || type === 'number') {
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
pad.diagnosticInfo.socket[i] = value;
|
||||
}
|
||||
}
|
||||
|
@ -734,7 +752,7 @@ export class Pad {
|
|||
}
|
||||
handleIsFullyConnected
|
||||
=
|
||||
(isConnected, isInitialConnect) => {
|
||||
(isConnected: boolean, isInitialConnect: boolean) => {
|
||||
pad.determineChatVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineAuthorshipColorsVisibility();
|
||||
|
@ -744,7 +762,7 @@ export class Pad {
|
|||
}
|
||||
determineChatVisibility
|
||||
=
|
||||
(asNowConnectedFeedback) => {
|
||||
(asNowConnectedFeedback: boolean) => {
|
||||
const chatVisCookie = padcookie.getPref('chatAlwaysVisible');
|
||||
if (chatVisCookie) { // if the cookie is set for chat always visible
|
||||
chat.stickToScreen(true); // stick it to the screen
|
||||
|
@ -755,7 +773,7 @@ export class Pad {
|
|||
}
|
||||
determineChatAndUsersVisibility
|
||||
=
|
||||
(asNowConnectedFeedback) => {
|
||||
(asNowConnectedFeedback: boolean) => {
|
||||
const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible');
|
||||
if (chatAUVisCookie) { // if the cookie is set for chat always visible
|
||||
chat.chatAndUsers(true); // stick it to the screen
|
||||
|
@ -777,7 +795,7 @@ export class Pad {
|
|||
}
|
||||
handleCollabAction
|
||||
=
|
||||
(action) => {
|
||||
(action: string) => {
|
||||
if (action === 'commitPerformed') {
|
||||
padeditbar.setSyncStatus('syncing');
|
||||
} else if (action === 'newlyIdle') {
|
||||
|
@ -806,26 +824,27 @@ export class Pad {
|
|||
=
|
||||
() => {
|
||||
$('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.missedChanges')
|
||||
.val(JSON.stringify(pad.collabClient.getMissedChanges()));
|
||||
.val(JSON.stringify(pad.collabClient!.getMissedChanges()));
|
||||
$('form#reconnectform').trigger('submit');
|
||||
}
|
||||
callWhenNotCommitting
|
||||
=
|
||||
(f) => {
|
||||
pad.collabClient.callWhenNotCommitting(f);
|
||||
(f: Function) => {
|
||||
pad.collabClient!.callWhenNotCommitting(f);
|
||||
}
|
||||
getCollabRevisionNumber
|
||||
=
|
||||
() => pad.collabClient.getCurrentRevisionNumber()
|
||||
() => pad.collabClient!.getCurrentRevisionNumber()
|
||||
isFullyConnected
|
||||
=
|
||||
() => padconnectionstatus.isFullyConnected()
|
||||
addHistoricalAuthors
|
||||
=
|
||||
(data) => {
|
||||
(data: HistoricalAuthorData) => {
|
||||
if (!pad.collabClient) {
|
||||
window.setTimeout(() => {
|
||||
pad.addHistoricalAuthors(data);
|
||||
|
@ -849,9 +868,7 @@ export type PadSettings = {
|
|||
|
||||
export const pad = new Pad()
|
||||
|
||||
|
||||
exports.baseURL = '';
|
||||
exports.randomString = randomString;
|
||||
exports.getParams = getParams;
|
||||
exports.pad = pad;
|
||||
exports.init = init;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
'use strict';
|
||||
import html10n from './vendors/html10n';
|
||||
import {PadOption} from "./types/SocketIOMessage";
|
||||
import {Pad} from "./pad";
|
||||
|
||||
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
|
||||
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
||||
export const showCountDownTimerToReconnectOnModal = ($modal: JQuery<HTMLElement>, pad: Pad) => {
|
||||
if (window.clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
|
||||
createCountDownElementsIfNecessary($modal);
|
||||
|
||||
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;
|
||||
if (elementsDoNotExist) {
|
||||
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));
|
||||
};
|
||||
|
||||
const createTimerForModal = ($modal, pad) => {
|
||||
const createTimerForModal = ($modal: JQuery<HTMLElement>, pad: Pad) => {
|
||||
const timeUntilReconnection =
|
||||
clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
|
||||
window.clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
|
||||
const timer = new CountDownTimer(timeUntilReconnection);
|
||||
|
||||
timer.onTick((minutes, seconds) => {
|
||||
timer.onTick((minutes: number, seconds: number) => {
|
||||
updateCountDownTimerMessage($modal, minutes, seconds);
|
||||
}).onExpire(() => {
|
||||
const wasANetworkError = $modal.is('.disconnected');
|
||||
|
@ -72,23 +74,23 @@ const createTimerForModal = ($modal, pad) => {
|
|||
return timer;
|
||||
};
|
||||
|
||||
const disableAutomaticReconnection = ($modal) => {
|
||||
const disableAutomaticReconnection = ($modal: JQuery<HTMLElement>) => {
|
||||
toggleAutomaticReconnectionOption($modal, true);
|
||||
};
|
||||
const enableAutomaticReconnection = ($modal) => {
|
||||
const enableAutomaticReconnection = ($modal: JQuery<HTMLElement>) => {
|
||||
toggleAutomaticReconnectionOption($modal, false);
|
||||
};
|
||||
const toggleAutomaticReconnectionOption = ($modal, disableAutomaticReconnect) => {
|
||||
const toggleAutomaticReconnectionOption = ($modal: JQuery<HTMLElement>, disableAutomaticReconnect: boolean) => {
|
||||
$modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect);
|
||||
$modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect);
|
||||
};
|
||||
|
||||
const waitUntilClientCanConnectToServerAndThen = (callback, pad) => {
|
||||
const waitUntilClientCanConnectToServerAndThen = (callback: Function, pad: Pad) => {
|
||||
whenConnectionIsRestablishedWithServer(callback, pad);
|
||||
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
|
||||
// on every unsuccessful try
|
||||
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');
|
||||
};
|
||||
|
||||
const updateCountDownTimerMessage = ($modal, minutes, seconds) => {
|
||||
minutes = minutes < 10 ? `0${minutes}` : minutes;
|
||||
seconds = seconds < 10 ? `0${seconds}` : seconds;
|
||||
const updateCountDownTimerMessage = ($modal: JQuery<HTMLElement>, minutes: number, seconds: number) => {
|
||||
let minutesFormatted = minutes < 10 ? `0${minutes}` : minutes;
|
||||
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
|
||||
|
@ -125,71 +127,75 @@ const reconnectionTries = {
|
|||
// duration: how many **seconds** until the timer ends
|
||||
// granularity (optional): how many **milliseconds**
|
||||
// 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 = [];
|
||||
this.onExpireCallbacks = [];
|
||||
};
|
||||
class CountDownTimer {
|
||||
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 () {
|
||||
if (this.running) {
|
||||
return;
|
||||
this.onTickCallbacks = [];
|
||||
this.onExpireCallbacks = [];
|
||||
}
|
||||
this.running = true;
|
||||
const start = Date.now();
|
||||
const that = this;
|
||||
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();
|
||||
start = ()=> {
|
||||
if (this.running) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
timer();
|
||||
};
|
||||
this.running = true;
|
||||
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) {
|
||||
const obj = CountDownTimer.parse(diff);
|
||||
this.onTickCallbacks.forEach(function (callback) {
|
||||
callback.call(this, obj.minutes, obj.seconds);
|
||||
}, this);
|
||||
};
|
||||
CountDownTimer.prototype.expire = function () {
|
||||
this.onExpireCallbacks.forEach(function (callback) {
|
||||
callback.call(this);
|
||||
}, this);
|
||||
};
|
||||
|
||||
CountDownTimer.prototype.onTick = function (callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.onTickCallbacks.push(callback);
|
||||
if (diff > 0) {
|
||||
that.timeoutId = setTimeout(timer, that.granularity);
|
||||
that.tick(diff);
|
||||
} else {
|
||||
that.running = false;
|
||||
that.tick(0);
|
||||
that.expire();
|
||||
}
|
||||
};
|
||||
timer();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
CountDownTimer.prototype.onExpire = function (callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.onExpireCallbacks.push(callback);
|
||||
tick = (diff: number)=> {
|
||||
const obj = this.parse(diff);
|
||||
this.onTickCallbacks.forEach( (callback)=> {
|
||||
callback.call(this, obj.minutes, obj.seconds);
|
||||
}, this);
|
||||
}
|
||||
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 () {
|
||||
this.running = false;
|
||||
clearTimeout(this.timeoutId);
|
||||
return this;
|
||||
};
|
||||
|
||||
CountDownTimer.parse = (seconds) => ({
|
||||
minutes: (seconds / 60) | 0,
|
||||
seconds: (seconds % 60) | 0,
|
||||
});
|
||||
onExpire = (callback: Function)=> {
|
||||
if (typeof callback === 'function') {
|
||||
this.onExpireCallbacks.push(callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
cancel = () => {
|
||||
this.running = false;
|
||||
clearTimeout(this.timeoutId);
|
||||
return this;
|
||||
}
|
||||
parse = (seconds: number) => ({
|
||||
minutes: (seconds / 60) | 0,
|
||||
seconds: (seconds % 60) | 0,
|
||||
});
|
||||
}
|
|
@ -22,45 +22,51 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const padmodals = require('./pad_modals').padmodals;
|
||||
import {padModals} from "./pad_modals";
|
||||
|
||||
const padconnectionstatus = (() => {
|
||||
let status = {
|
||||
class PadConnectionStatus {
|
||||
private status: {
|
||||
what: string,
|
||||
why?: string
|
||||
} = {
|
||||
what: 'connecting',
|
||||
};
|
||||
}
|
||||
|
||||
const self = {
|
||||
init: () => {
|
||||
$('button#forcereconnect').on('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
connected: () => {
|
||||
status = {
|
||||
what: 'connected',
|
||||
};
|
||||
padmodals.showModal('connected');
|
||||
padmodals.hideOverlay();
|
||||
},
|
||||
reconnecting: () => {
|
||||
status = {
|
||||
what: 'reconnecting',
|
||||
};
|
||||
init = () => {
|
||||
$('button#forcereconnect').on('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
padmodals.showModal('reconnecting');
|
||||
padmodals.showOverlay();
|
||||
},
|
||||
disconnected: (msg) => {
|
||||
if (status.what === 'disconnected') return;
|
||||
connected = () => {
|
||||
this.status = {
|
||||
what: 'connected',
|
||||
};
|
||||
padModals.showModal('connected');
|
||||
padModals.hideOverlay();
|
||||
}
|
||||
reconnecting = () => {
|
||||
this.status = {
|
||||
what: 'reconnecting',
|
||||
};
|
||||
|
||||
status = {
|
||||
what: 'disconnected',
|
||||
why: msg,
|
||||
};
|
||||
padModals.showModal('reconnecting');
|
||||
padModals.showOverlay();
|
||||
}
|
||||
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
|
||||
// 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.
|
||||
this.status =
|
||||
{
|
||||
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 = [
|
||||
'badChangeset',
|
||||
'corruptPad',
|
||||
|
@ -80,13 +86,17 @@ const padconnectionstatus = (() => {
|
|||
k = 'disconnected';
|
||||
}
|
||||
|
||||
padmodals.showModal(k);
|
||||
padmodals.showOverlay();
|
||||
},
|
||||
isFullyConnected: () => status.what === 'connected',
|
||||
getStatus: () => status,
|
||||
};
|
||||
return self;
|
||||
})();
|
||||
padModals.showModal(k);
|
||||
padModals.showOverlay();
|
||||
}
|
||||
isFullyConnected
|
||||
=
|
||||
() => this.status.what === 'connected'
|
||||
getStatus
|
||||
=
|
||||
() => this.status
|
||||
}
|
||||
|
||||
|
||||
export const padconnectionstatus = new PadConnectionStatus()
|
||||
exports.padconnectionstatus = padconnectionstatus;
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {MapArrayType} from "../../node/types/MapType";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -26,13 +28,16 @@ const browser = require('./vendors/browser');
|
|||
const hooks = require('./pluginfw/hooks');
|
||||
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 _ = require('underscore');
|
||||
require('./vendors/nice-select');
|
||||
|
||||
class ToolbarItem {
|
||||
constructor(element) {
|
||||
private $el: JQuery<HTMLElement>;
|
||||
constructor(element: JQuery<HTMLElement>) {
|
||||
this.$el = element;
|
||||
}
|
||||
|
||||
|
@ -46,8 +51,9 @@ class ToolbarItem {
|
|||
}
|
||||
}
|
||||
|
||||
setValue(val) {
|
||||
setValue(val: boolean) {
|
||||
if (this.isSelect()) {
|
||||
// @ts-ignore
|
||||
return this.$el.find('select').val(val);
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +70,7 @@ class ToolbarItem {
|
|||
return this.getType() === 'button';
|
||||
}
|
||||
|
||||
bind(callback) {
|
||||
bind(callback: (cmd: string|undefined, tb: ToolbarItem)=>void) {
|
||||
if (this.isButton()) {
|
||||
this.$el.on('click', (event) => {
|
||||
$(':focus').trigger('blur');
|
||||
|
@ -79,52 +85,62 @@ class ToolbarItem {
|
|||
}
|
||||
}
|
||||
|
||||
const syncAnimation = (() => {
|
||||
const SYNCING = -100;
|
||||
const DONE = 100;
|
||||
let state = DONE;
|
||||
const fps = 25;
|
||||
const step = 1 / fps;
|
||||
const T_START = -0.5;
|
||||
const T_FADE = 1.0;
|
||||
const T_GONE = 1.5;
|
||||
const animator = padutils.makeAnimationScheduler(() => {
|
||||
if (state === SYNCING || state === DONE) {
|
||||
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();
|
||||
},
|
||||
};
|
||||
})();
|
||||
class SyncAnimation {
|
||||
static SYNCING = -100;
|
||||
static DONE = 100;
|
||||
state = SyncAnimation.DONE;
|
||||
fps = 25;
|
||||
step = 1 / this.fps;
|
||||
static T_START = -0.5;
|
||||
static T_FADE = 1.0;
|
||||
static T_GONE = 1.5;
|
||||
private animator: { scheduleAnimation: () => void };
|
||||
constructor() {
|
||||
|
||||
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() {
|
||||
this._editbarPosition = 0;
|
||||
this.commands = {};
|
||||
|
@ -137,7 +153,7 @@ exports.padeditbar = new class {
|
|||
$('#editbar [data-key]').each((i, elt) => {
|
||||
$(elt).off('click');
|
||||
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
|
||||
*/
|
||||
if (!browser.safari) {
|
||||
// @ts-ignore
|
||||
$('select').niceSelect();
|
||||
}
|
||||
|
||||
// When editor is scrolled, we add a class to style the editbar differently
|
||||
$('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; }
|
||||
|
@ -180,25 +197,25 @@ exports.padeditbar = new class {
|
|||
enable() {
|
||||
$('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar');
|
||||
}
|
||||
registerCommand(cmd, callback) {
|
||||
registerCommand(cmd: string, callback: (cmd: string, ace: Ace2Editor, item: ToolbarItem)=>void) {
|
||||
this.commands[cmd] = callback;
|
||||
return this;
|
||||
}
|
||||
registerDropdownCommand(cmd, dropdown) {
|
||||
registerDropdownCommand(cmd: string, dropdown?: string) {
|
||||
dropdown = dropdown || cmd;
|
||||
this.dropdowns.push(dropdown);
|
||||
this.registerCommand(cmd, () => {
|
||||
this.toggleDropDown(dropdown);
|
||||
});
|
||||
}
|
||||
registerAceCommand(cmd, callback) {
|
||||
registerAceCommand(cmd: string, callback: ToolbarAceCallback) {
|
||||
this.registerCommand(cmd, (cmd, ace, item) => {
|
||||
ace.callWithAce((ace) => {
|
||||
callback(cmd, ace, item);
|
||||
}, cmd, true);
|
||||
});
|
||||
}
|
||||
triggerCommand(cmd, item) {
|
||||
triggerCommand(cmd: string, item: ToolbarItem) {
|
||||
if (this.isEnabled() && this.commands[cmd]) {
|
||||
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).
|
||||
toggleDropDown(moduleName, cb = null) {
|
||||
let cbErr = null;
|
||||
toggleDropDown(moduleName: string, cb:Function|null = null) {
|
||||
let cbErr: Error|null = null;
|
||||
try {
|
||||
// do nothing if users are sticked
|
||||
if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) {
|
||||
|
@ -249,12 +266,13 @@ exports.padeditbar = new class {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
cbErr = err || new Error(err);
|
||||
} finally {
|
||||
if (cb) Promise.resolve().then(() => cb(cbErr));
|
||||
}
|
||||
}
|
||||
setSyncStatus(status) {
|
||||
setSyncStatus(status: string) {
|
||||
if (status === 'syncing') {
|
||||
syncAnimation.syncing();
|
||||
} else if (status === 'done') {
|
||||
|
@ -269,7 +287,7 @@ exports.padeditbar = new class {
|
|||
if ($('#readonlyinput').is(':checked')) {
|
||||
const urlParts = padUrl.split('/');
|
||||
urlParts.pop();
|
||||
const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`;
|
||||
const readonlyLink = `${urlParts.join('/')}/${window.clientVars.readOnlyId}`;
|
||||
$('#embedinput')
|
||||
.val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`);
|
||||
$('#linkinput').val(readonlyLink);
|
||||
|
@ -288,16 +306,16 @@ exports.padeditbar = new class {
|
|||
// this is approximate, we cannot measure it because on mobile
|
||||
// Layout it takes the full width on the bottom of the page
|
||||
const menuRightWidth = 280;
|
||||
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width() - menuRightWidth ||
|
||||
$('.toolbar').width() < 1000) {
|
||||
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()! - menuRightWidth ||
|
||||
$('.toolbar').width()! < 1000) {
|
||||
$('body').addClass('mobile-layout');
|
||||
}
|
||||
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()) {
|
||||
if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()!) {
|
||||
$('.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
|
||||
// Send the users focus back to the pad
|
||||
if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) {
|
||||
|
@ -313,15 +331,17 @@ exports.padeditbar = new class {
|
|||
// Timeslider probably..
|
||||
$('#editorcontainerbox').trigger('focus'); // Focus back onto the pad
|
||||
} 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
|
||||
evt.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// Focus on the editbar :)
|
||||
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');
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
@ -337,7 +357,8 @@ exports.padeditbar = new class {
|
|||
// On left arrow move to next button in editbar
|
||||
if (evt.keyCode === 37) {
|
||||
// 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--;
|
||||
// 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
|
||||
if (evt.keyCode === 39) {
|
||||
// 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;
|
||||
|
||||
this._editbarPosition++;
|
||||
|
@ -394,14 +416,14 @@ exports.padeditbar = new class {
|
|||
});
|
||||
|
||||
this.registerCommand('savedRevision', () => {
|
||||
padsavedrevs.saveNow();
|
||||
padsavedrevs.saveNow(pad);
|
||||
});
|
||||
|
||||
this.registerCommand('showTimeSlider', () => {
|
||||
document.location = `${document.location.pathname}/timeslider`;
|
||||
});
|
||||
|
||||
const aceAttributeCommand = (cmd, ace) => {
|
||||
const aceAttributeCommand = (cmd: string, ace: any) => {
|
||||
ace.ace_toggleAttributeOnSelection(cmd);
|
||||
};
|
||||
this.registerAceCommand('bold', aceAttributeCommand);
|
||||
|
@ -479,4 +501,6 @@ exports.padeditbar = new class {
|
|||
}
|
||||
});
|
||||
}
|
||||
}();
|
||||
};
|
||||
|
||||
export const padeditbar = new PadEditor()
|
|
@ -31,12 +31,13 @@ import {padUtils as padutils} from "./pad_utils";
|
|||
import {Ace2Editor} from "./ace";
|
||||
import html10n from '../js/vendors/html10n'
|
||||
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 {
|
||||
private pad?: PadType
|
||||
private settings: undefined| ClientVarData
|
||||
private ace: any
|
||||
private pad?: Pad
|
||||
private settings: undefined| PadOption
|
||||
ace: Ace2Editor|null
|
||||
private viewZoom: number
|
||||
|
||||
constructor() {
|
||||
|
@ -47,7 +48,7 @@ export class PadEditor {
|
|||
this.viewZoom = 100
|
||||
}
|
||||
|
||||
init = async (initialViewOptions: MapArrayType<string>, _pad: PadType) => {
|
||||
init = async (initialViewOptions: MapArrayType<boolean>, _pad: Pad) => {
|
||||
this.pad = _pad;
|
||||
this.settings = this.pad.settings;
|
||||
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 value = String(newOptions[key]);
|
||||
if (value === 'true') return true;
|
||||
|
@ -136,25 +137,25 @@ export class PadEditor {
|
|||
let v;
|
||||
|
||||
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
|
||||
this.ace.setProperty('rtlIsTrue', v);
|
||||
this.ace!.setProperty('rtlIsTrue', v);
|
||||
padutils.setCheckbox($('#options-rtlcheck'), v);
|
||||
|
||||
v = getOption('showLineNumbers', true);
|
||||
this.ace.setProperty('showslinenumbers', v);
|
||||
this.ace!.setProperty('showslinenumbers', v);
|
||||
padutils.setCheckbox($('#options-linenoscheck'), v);
|
||||
|
||||
v = getOption('showAuthorColors', true);
|
||||
this.ace.setProperty('showsauthorcolors', v);
|
||||
this.ace!.setProperty('showsauthorcolors', v);
|
||||
$('#chattext').toggleClass('authorColors', v);
|
||||
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
|
||||
padutils.setCheckbox($('#options-colorscheck'), v);
|
||||
|
||||
// Override from parameters if true
|
||||
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 = () => {
|
||||
|
@ -173,12 +174,12 @@ export class PadEditor {
|
|||
this.ace.setEditable(false);
|
||||
}
|
||||
}
|
||||
restoreRevisionText= (dataFromServer: ClientVarData) => {
|
||||
restoreRevisionText= (dataFromServer: ClientVarPayload) => {
|
||||
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
|
||||
const lineNumber = window.location.hash.substr(1);
|
||||
if (lineNumber) {
|
||||
|
|
|
@ -23,29 +23,27 @@
|
|||
*/
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
import {Pad} from "./pad";
|
||||
|
||||
|
||||
const padimpexp = (() => {
|
||||
let pad;
|
||||
class PadImpExp {
|
||||
private pad?: Pad;
|
||||
|
||||
// /// import
|
||||
const addImportFrames = () => {
|
||||
addImportFrames = () => {
|
||||
$('#import .importframe').remove();
|
||||
const iframe = $('<iframe>')
|
||||
.css('display', 'none')
|
||||
.attr('name', 'importiframe')
|
||||
.addClass('importframe');
|
||||
.css('display', 'none')
|
||||
.attr('name', 'importiframe')
|
||||
.addClass('importframe');
|
||||
$('#import').append(iframe);
|
||||
};
|
||||
|
||||
const fileInputUpdated = () => {
|
||||
}
|
||||
fileInputUpdated = () => {
|
||||
$('#importsubmitinput').addClass('throbbold');
|
||||
$('#importformfilediv').addClass('importformenabled');
|
||||
$('#importsubmitinput').prop('disabled', false);
|
||||
$('#importmessagefail').fadeOut('fast');
|
||||
};
|
||||
|
||||
const fileInputSubmit = function (e) {
|
||||
}
|
||||
fileInputSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
$('#importmessagefail').fadeOut('fast');
|
||||
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return;
|
||||
|
@ -54,10 +52,13 @@ const padimpexp = (() => {
|
|||
$('#importarrow').stop(true, true).hide();
|
||||
$('#importstatusball').show();
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
const {code, message, data: {directDatabaseAccess} = {}} = await $.ajax({
|
||||
url: `${window.location.href.split('?')[0].split('#')[0]}/import`,
|
||||
method: 'POST',
|
||||
data: new FormData(this),
|
||||
// FIXME is this correct
|
||||
// @ts-ignore
|
||||
data: new FormData(this.fileInputSubmit),
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'json',
|
||||
|
@ -67,7 +68,7 @@ const padimpexp = (() => {
|
|||
return {code: 2, message: 'Unknown import error'};
|
||||
});
|
||||
if (code !== 0) {
|
||||
importErrorMessage(message);
|
||||
this.importErrorMessage(message);
|
||||
} else {
|
||||
$('#import_export').removeClass('popup-show');
|
||||
if (directDatabaseAccess) window.location.reload();
|
||||
|
@ -75,11 +76,11 @@ const padimpexp = (() => {
|
|||
$('#importsubmitinput').prop('disabled', false).val(html10n.get('pad.impexp.importbutton'));
|
||||
window.setTimeout(() => $('#importfileinput').prop('disabled', false), 0);
|
||||
$('#importstatusball').hide();
|
||||
addImportFrames();
|
||||
this.addImportFrames();
|
||||
})();
|
||||
};
|
||||
}
|
||||
|
||||
const importErrorMessage = (status) => {
|
||||
importErrorMessage = (status: string) => {
|
||||
const known = [
|
||||
'convertFailed',
|
||||
'uploadFailed',
|
||||
|
@ -89,12 +90,12 @@ const padimpexp = (() => {
|
|||
];
|
||||
const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`);
|
||||
|
||||
const showError = (fade) => {
|
||||
const showError = (fade?: boolean) => {
|
||||
const popup = $('#importmessagefail').empty()
|
||||
.append($('<strong>')
|
||||
.css('color', 'red')
|
||||
.text(`${html10n.get('pad.impexp.importfailed')}: `))
|
||||
.append(document.createTextNode(msg));
|
||||
.append($('<strong>')
|
||||
.css('color', 'red')
|
||||
.text(`${html10n.get('pad.impexp.importfailed')}: `))
|
||||
.append(document.createTextNode(msg));
|
||||
popup[(fade ? 'fadeIn' : 'show')]();
|
||||
};
|
||||
|
||||
|
@ -104,83 +105,83 @@ const padimpexp = (() => {
|
|||
} else {
|
||||
showError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// /// export
|
||||
|
||||
function cantExport() {
|
||||
cantExport = () => {
|
||||
let type = $(this);
|
||||
if (type.hasClass('exporthrefpdf')) {
|
||||
// @ts-ignore
|
||||
type = 'PDF';
|
||||
} else if (type.hasClass('exporthrefdoc')) {
|
||||
// @ts-ignore
|
||||
type = 'Microsoft Word';
|
||||
} else if (type.hasClass('exporthrefodt')) {
|
||||
// @ts-ignore
|
||||
type = 'OpenDocument';
|
||||
} else {
|
||||
// @ts-ignore
|
||||
type = 'this file';
|
||||
}
|
||||
alert(html10n.get('pad.impexp.exportdisabled', {type}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ///
|
||||
const self = {
|
||||
init: (_pad) => {
|
||||
pad = _pad;
|
||||
init = (_pad: Pad) => {
|
||||
this.pad = _pad;
|
||||
|
||||
// get /p/padname
|
||||
// if /p/ isn't available due to a rewrite we use the clientVars padId
|
||||
const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId;
|
||||
// get /p/padname
|
||||
// if /p/ isn't available due to a rewrite we use the 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'));
|
||||
html10n.bind('localized', () => {
|
||||
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
|
||||
});
|
||||
});
|
||||
|
||||
// build the export links
|
||||
$('#exporthtmla').attr('href', `${padRootPath}/export/html`);
|
||||
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
|
||||
$('#exportplaina').attr('href', `${padRootPath}/export/txt`);
|
||||
// build the export links
|
||||
$('#exporthtmla').attr('href', `${padRootPath}/export/html`);
|
||||
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
|
||||
$('#exportplaina').attr('href', `${padRootPath}/export/txt`);
|
||||
|
||||
// hide stuff thats not avaible if abiword/soffice is disabled
|
||||
if (clientVars.exportAvailable === 'no') {
|
||||
$('#exportworda').remove();
|
||||
$('#exportpdfa').remove();
|
||||
$('#exportopena').remove();
|
||||
// hide stuff thats not avaible if abiword/soffice is disabled
|
||||
if (window.clientVars.exportAvailable === 'no') {
|
||||
$('#exportworda').remove();
|
||||
$('#exportpdfa').remove();
|
||||
$('#exportopena').remove();
|
||||
|
||||
$('#importmessageabiword').show();
|
||||
} else if (clientVars.exportAvailable === 'withoutPDF') {
|
||||
$('#exportpdfa').remove();
|
||||
$('#importmessageabiword').show();
|
||||
} else if (window.clientVars.exportAvailable === 'withoutPDF') {
|
||||
$('#exportpdfa').remove();
|
||||
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
|
||||
$('#importexport').css({height: '142px'});
|
||||
$('#importexportline').css({height: '142px'});
|
||||
} else {
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
}
|
||||
$('#importexport').css({height: '142px'});
|
||||
$('#importexportline').css({height: '142px'});
|
||||
} else {
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
}
|
||||
|
||||
addImportFrames();
|
||||
$('#importfileinput').on('change', fileInputUpdated);
|
||||
$('#importform').off('submit').on('submit', fileInputSubmit);
|
||||
$('.disabledexport').on('click', 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;
|
||||
})();
|
||||
this.addImportFrames();
|
||||
$('#importfileinput').on('change', this.fileInputUpdated);
|
||||
$('#importform').off('submit').on('submit', this.fileInputSubmit);
|
||||
$('.disabledexport').on('click', this.cantExport);
|
||||
}
|
||||
|
||||
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();
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {Pad} from "./pad";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -23,33 +25,34 @@
|
|||
*/
|
||||
|
||||
const padeditbar = require('./pad_editbar').padeditbar;
|
||||
const automaticReconnect = require('./pad_automatic_reconnect');
|
||||
import {showCountDownTimerToReconnectOnModal} from './pad_automatic_reconnect'
|
||||
|
||||
const padmodals = (() => {
|
||||
let pad = undefined;
|
||||
const self = {
|
||||
init: (_pad) => {
|
||||
pad = _pad;
|
||||
},
|
||||
showModal: (messageId) => {
|
||||
padeditbar.toggleDropDown('none');
|
||||
$('#connectivity .visible').removeClass('visible');
|
||||
$(`#connectivity .${messageId}`).addClass('visible');
|
||||
class PadModals {
|
||||
private pad?: Pad
|
||||
|
||||
const $modal = $(`#connectivity .${messageId}`);
|
||||
automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad);
|
||||
constructor() {
|
||||
}
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
return self;
|
||||
})();
|
||||
init = (pad: Pad) => {
|
||||
this.pad = pad
|
||||
}
|
||||
showModal = (messageId: string) => {
|
||||
padeditbar.toggleDropDown('none');
|
||||
$('#connectivity .visible').removeClass('visible');
|
||||
$(`#connectivity .${messageId}`).addClass('visible');
|
||||
|
||||
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()
|
|
@ -1,5 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
import html10n from "./vendors/html10n";
|
||||
import {Pad} from "./pad";
|
||||
|
||||
/**
|
||||
* Copyright 2012 Peter 'Pita' Martischka
|
||||
*
|
||||
|
@ -16,10 +19,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
let pad;
|
||||
|
||||
exports.saveNow = () => {
|
||||
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
|
||||
export const saveNow = (pad: Pad) => {
|
||||
pad!.collabClient!.sendMessage({type: 'SAVE_REVISION'});
|
||||
window.$.gritter.add({
|
||||
// (string | mandatory) the heading of the notification
|
||||
title: html10n.get('pad.savedrevs.marked'),
|
||||
|
@ -32,7 +33,3 @@ exports.saveNow = () => {
|
|||
class_name: 'saved-revision',
|
||||
});
|
||||
};
|
||||
|
||||
exports.init = (_pad) => {
|
||||
pad = _pad;
|
||||
};
|
|
@ -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(' ')))
|
||||
.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;
|
661
src/static/js/pad_userlist.ts
Normal file
661
src/static/js/pad_userlist.ts
Normal 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(' ')))
|
||||
.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()
|
|
@ -10,7 +10,7 @@ import {Socket} from "socket.io";
|
|||
* https://socket.io/docs/v2/client-api/#new-Manager-url-options
|
||||
* @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
|
||||
// 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
|
||||
|
@ -28,15 +28,10 @@ const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): Socke
|
|||
};
|
||||
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) => {
|
||||
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;
|
||||
|
|
|
@ -31,10 +31,10 @@ const hooks = require('./pluginfw/hooks');
|
|||
import connect from './socketio'
|
||||
import html10n from '../js/vendors/html10n'
|
||||
import {Socket} from "socket.io";
|
||||
import {ClientVarMessage, SocketIOMessage} from "./types/SocketIOMessage";
|
||||
import {ClientVarData, ClientVarMessage, ClientVarPayload, SocketIOMessage} from "./types/SocketIOMessage";
|
||||
import {Func} from "mocha";
|
||||
|
||||
type ChangeSetLoader = {
|
||||
export type ChangeSetLoader = {
|
||||
handleMessageFromServer(msg: ClientVarMessage): void
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ export const init = () => {
|
|||
socket.on('message', (message: ClientVarMessage) => {
|
||||
if (message.type === 'CLIENT_VARS') {
|
||||
handleClientVars(message);
|
||||
} else if (message.accessStatus) {
|
||||
} else if ("accessStatus" in message) {
|
||||
$('body').html('<h2>You have no permission to access this pad</h2>');
|
||||
} else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') {
|
||||
changesetLoader.handleMessageFromServer(message);
|
||||
|
@ -111,7 +111,7 @@ const sendSocketMsg = (type: string, data: Object) => {
|
|||
|
||||
const fireWhenAllScriptsAreLoaded: Function[] = [];
|
||||
|
||||
const handleClientVars = (message: ClientVarMessage) => {
|
||||
const handleClientVars = (message: ClientVarData) => {
|
||||
// save the client Vars
|
||||
window.clientVars = message.data;
|
||||
|
||||
|
|
|
@ -1,43 +1,209 @@
|
|||
import {MapArrayType} from "../../../node/types/MapType";
|
||||
import {AText} from "./AText";
|
||||
import AttributePool from "../AttributePool";
|
||||
import attributePool from "../AttributePool";
|
||||
|
||||
export type SocketIOMessage = {
|
||||
type: 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,
|
||||
historicalAuthorData:MapArrayType<{
|
||||
name: string;
|
||||
colorId: string;
|
||||
}>,
|
||||
historicalAuthorData: HistoricalAuthorData,
|
||||
atext: AText,
|
||||
apool: AttributePool,
|
||||
noColors: boolean,
|
||||
userName: string,
|
||||
userColor:string,
|
||||
userColor: number,
|
||||
hideChat: boolean,
|
||||
padOptions: MapArrayType<string>,
|
||||
padOptions: PadOption,
|
||||
padId: string,
|
||||
clientIp: string,
|
||||
colorPalette: MapArrayType<string>,
|
||||
colorPalette: MapArrayType<number>,
|
||||
accountPrivs: MapArrayType<string>,
|
||||
collab_client_vars: MapArrayType<string>,
|
||||
collab_client_vars: ServerVar,
|
||||
chatHead: number,
|
||||
readonly: boolean,
|
||||
serverTimestamp: number,
|
||||
initialOptions: MapArrayType<string>,
|
||||
userId: string,
|
||||
mode: string,
|
||||
randomVersionString: string,
|
||||
skinName: string
|
||||
skinVariants: string,
|
||||
exportAvailable: string
|
||||
}
|
||||
|
||||
export type ClientVarMessage = {
|
||||
data: ClientVarData,
|
||||
type: string
|
||||
accessStatus: string
|
||||
export type ClientVarData = {
|
||||
type: "CLIENT_VARS"
|
||||
data: ClientVarPayload
|
||||
}
|
||||
|
||||
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 = {
|
||||
type: string
|
||||
component: string
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import {ClientVarData} from "./SocketIOMessage";
|
||||
import {ClientVarData, ClientVarPayload} from "./SocketIOMessage";
|
||||
import {Pad} from "../pad";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
clientVars: ClientVarData;
|
||||
clientVars: ClientVarPayload;
|
||||
$: any,
|
||||
customStart?:any
|
||||
customStart?:any,
|
||||
ajlog: string
|
||||
}
|
||||
let pad: Pad
|
||||
}
|
||||
|
|
37
src/static/js/vendors/BrowserType.ts
vendored
Normal file
37
src/static/js/vendors/BrowserType.ts
vendored
Normal 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
|
||||
}
|
310
src/static/js/vendors/browser.js
vendored
310
src/static/js/vendors/browser.js
vendored
|
@ -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
216
src/static/js/vendors/browser.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
1
src/static/js/vendors/gritter.js
vendored
1
src/static/js/vendors/gritter.js
vendored
|
@ -343,7 +343,6 @@
|
|||
$('#editorcontainerbox').append(this._tpl_wrap_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})(jQuery);
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
const basePath = new URL('..', window.location.href).pathname;
|
||||
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');
|
||||
pad.baseURL = basePath;
|
||||
window.plugins = require('../../src/static/js/pluginfw/client_plugins');
|
||||
|
@ -23,8 +23,8 @@
|
|||
// TODO: These globals shouldn't exist.
|
||||
window.pad = pad.pad;
|
||||
window.chat = require('../../src/static/js/chat').chat;
|
||||
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
|
||||
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
|
||||
window.padeditbar = require('../static/js/pad_editbar').padeditbar;
|
||||
window.padimpexp = require('../static/js/pad_impexp').padimpexp;
|
||||
await import('../../src/static/js/skin_variants')
|
||||
await import('../../src/static/js/basic_error_handler')
|
||||
|
||||
|
|
|
@ -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
|
||||
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');
|
||||
const socket = timeSlider.socket;
|
||||
|
@ -31,8 +31,8 @@ import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
|
|||
/* TODO: These globals shouldn't exist. */
|
||||
|
||||
});
|
||||
const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
||||
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
|
||||
const padeditbar = require('src/static/js/pad_editbar').padeditbar;
|
||||
const padimpexp = require('src/static/js/pad_impexp').padimpexp;
|
||||
setBaseURl(baseURL)
|
||||
timeSlider.init();
|
||||
padeditbar.init()
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import {MapArrayType} from "../../../node/types/MapType";
|
||||
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 assert = require('assert').strict;
|
||||
const common = require('../common');
|
||||
|
@ -13,7 +13,7 @@ const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
|||
const logger = common.logger;
|
||||
|
||||
type CheckFN = ({message, pad, padId}:{
|
||||
message?: typeof ChatMessage,
|
||||
message?: ChatMessage,
|
||||
pad?: typeof Pad,
|
||||
padId?: string,
|
||||
})=>void;
|
||||
|
@ -103,10 +103,10 @@ describe(__filename, function () {
|
|||
checkHook('chatNewMessage', ({message}) => {
|
||||
assert(message != null);
|
||||
assert(message instanceof ChatMessage);
|
||||
assert.equal(message.authorId, authorId);
|
||||
assert.equal(message.text, this.test!.title);
|
||||
assert(message.time >= start);
|
||||
assert(message.time <= Date.now());
|
||||
assert.equal(message!.authorId, authorId);
|
||||
assert.equal(message!.text, this.test!.title);
|
||||
assert(message!.time! >= start);
|
||||
assert(message!.time! <= Date.now());
|
||||
}),
|
||||
sendChat(socket, {text: this.test!.title}),
|
||||
]);
|
||||
|
@ -153,8 +153,8 @@ describe(__filename, function () {
|
|||
const customMetadata = {foo: this.test!.title};
|
||||
await Promise.all([
|
||||
checkHook('chatNewMessage', ({message}) => {
|
||||
message.text = modifiedText;
|
||||
message.customMetadata = customMetadata;
|
||||
message!.text = modifiedText;
|
||||
message!.customMetadata = customMetadata;
|
||||
}),
|
||||
(async () => {
|
||||
const {message} = await listen('CHAT_MESSAGE');
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<script src="../../static/js/vendors/jquery.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="lib/underscore.js"></script>
|
||||
|
||||
|
|
Loading…
Reference in a new issue