mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* fix(pad): URL view-option params lost to padeditor.init race (#7840) `?showLineNumbers=false` and `?useMonospaceFont=true` were being silently clobbered shortly after pad load. Same race that affected `?rtl=false` before #7464: 1. _afterHandshake → getParams() sets settings.LineNumbersDisabled (or useMonospaceFontGlobal, noColors). 2. _afterHandshake calls padeditor.init(view).then(postAceInit) — async; ace iframes still loading. 3. Sync tail of _afterHandshake hits the URL-param overrides and calls changeViewOption('showLineNumbers', false) etc. These queue setProperty('showslinenumbers', false) in Ace2Editor's actionsPendingInit queue (loaded=false). 4. ace.init resolves → loaded=true → queue flushes → URL-driven value applied. 5. padeditor.init resumes past its own await and calls setViewOptions(initialViewOptions) — initialViewOptions is built from clientVars.initialOptions.view (server defaults ∨ cookie), which does NOT carry the URL preference. The resulting setProperty('showslinenumbers', true) runs against loaded=true ace and immediately re-shows the gutter. #7464 noticed this race for RTL and moved the override into postAceInit. The neighbouring blocks for showLineNumbers / noColors / useMonospaceFontGlobal were left at the synchronous-tail site — generalise the same fix to all three. Direct-browser users typically had a `prefs` cookie with showLineNumbers=false from a prior in-pad toggle, so the initialViewOptions value happened to match the URL param and the race was unobservable. Cross-context iframe embeds (the reporter's configuration) start with no cookie, so the server default true fights the URL false and the race becomes visible. Adds src/tests/frontend-new/specs/url_view_options.spec.ts covering the showLineNumbers=false / =true / useMonospaceFont=true cases on initial-load navigation (the path where the race actually fires). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(types): annotate navigateWithParam helper params Fixes CI ts-check: parameters page/padId/param implicitly had `any`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1094 lines
40 KiB
TypeScript
1094 lines
40 KiB
TypeScript
// @ts-nocheck
|
|
'use strict';
|
|
const skinVariants = require('./skin_variants');
|
|
|
|
/**
|
|
* 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., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
let socket;
|
|
|
|
|
|
// These jQuery things should create local references, but for now `require()`
|
|
// assigns to the global `$` and augments it with plugins.
|
|
require('./vendors/jquery');
|
|
require('./vendors/farbtastic');
|
|
require('./vendors/gritter');
|
|
|
|
import html10n from './vendors/html10n'
|
|
|
|
import {Cookies} from "./pad_utils";
|
|
|
|
const chat = require('./chat').chat;
|
|
const getCollabClient = require('./collab_client').getCollabClient;
|
|
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
|
const padcookie = require('./pad_cookie').padcookie;
|
|
const padeditbar = require('./pad_editbar').padeditbar;
|
|
const padMode = require('./pad_mode').padMode;
|
|
const padeditor = require('./pad_editor').padeditor;
|
|
const padimpexp = require('./pad_impexp').padimpexp;
|
|
const padmodals = require('./pad_modals').padmodals;
|
|
const padsavedrevs = require('./pad_savedrevs');
|
|
const paduserlist = require('./pad_userlist').paduserlist;
|
|
import padutils from './pad_utils'
|
|
const colorutils = require('./colorutils').colorutils;
|
|
import {randomString} from "./pad_utils";
|
|
const socketio = require('./socketio');
|
|
|
|
const hooks = require('./pluginfw/hooks');
|
|
import {showPrivacyBannerIfEnabled} from './privacy_banner';
|
|
import {maybeShowOutdatedNotice} from './pad_outdated_notice';
|
|
|
|
// This array represents all GET-parameters which can be used to change a setting.
|
|
// name: the parameter-name, eg `?noColors=true` => `noColors`
|
|
// checkVal: the callback is only executed when
|
|
// * the parameter was supplied and matches checkVal
|
|
// * the parameter was supplied and checkVal is null
|
|
// callback: the function to call when all above succeeds, `val` is the value supplied by the user
|
|
const getParameters = [
|
|
{
|
|
name: 'noColors',
|
|
checkVal: 'true',
|
|
callback: (val) => {
|
|
settings.noColors = true;
|
|
$('#clearAuthorship').hide();
|
|
},
|
|
},
|
|
{
|
|
name: 'fadeInactiveAuthorColors',
|
|
checkVal: 'false',
|
|
callback: (val) => {
|
|
if (!clientVars.initialOptions) return;
|
|
if (!clientVars.initialOptions.view) clientVars.initialOptions.view = {};
|
|
clientVars.initialOptions.view.fadeInactiveAuthorColors = false;
|
|
},
|
|
},
|
|
{
|
|
name: 'showControls',
|
|
checkVal: 'true',
|
|
callback: (val) => {
|
|
$('#editbar').css('display', 'flex');
|
|
},
|
|
},
|
|
{
|
|
// showMenuRight accepts 'true' or 'false'. Explicit 'false' hides the
|
|
// right-side toolbar (import/export/timeslider/settings/embed/home/
|
|
// users) — useful for iframe-embedded readonly pads where the menu
|
|
// is just chrome (issue #5182). Explicit 'true' forces the menu
|
|
// visible (a no-op against the default, kept for symmetry and for
|
|
// overriding any future caller-applied hide). Any other value is a
|
|
// no-op — the menu stays in its default state.
|
|
name: 'showMenuRight',
|
|
checkVal: null,
|
|
callback: (val) => {
|
|
if (val === 'false') {
|
|
$('#editbar .menu_right').hide();
|
|
} else if (val === 'true') {
|
|
$('#editbar .menu_right').show();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'showChat',
|
|
checkVal: null,
|
|
callback: (val) => {
|
|
clientVars.initialOptions.showChat = val !== 'false';
|
|
if (val === 'false') {
|
|
settings.hideChat = true;
|
|
chat.hide();
|
|
$('#chaticon').hide();
|
|
} else {
|
|
settings.hideChat = false;
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'showLineNumbers',
|
|
checkVal: 'false',
|
|
callback: (val) => {
|
|
settings.LineNumbersDisabled = true;
|
|
},
|
|
},
|
|
{
|
|
name: 'useMonospaceFont',
|
|
checkVal: 'true',
|
|
callback: (val) => {
|
|
settings.useMonospaceFontGlobal = true;
|
|
},
|
|
},
|
|
{
|
|
name: 'userName',
|
|
checkVal: null,
|
|
callback: (val) => {
|
|
// The default for globalUserName/globalUserColor is the boolean `false`
|
|
// (sentinel meaning "no enforced value"). Older settings.json files used
|
|
// boolean `false` for these options too, which getParams() coerces to
|
|
// the string "false" — that fooled the !== false sentinel checks at
|
|
// _afterHandshake and shipped the literal string "false" as the user's
|
|
// name and color (#7686). Reject the sentinel string here so URL
|
|
// parameters like ?userName=false also no-op.
|
|
if (!val || val === 'false') return;
|
|
settings.globalUserName = val;
|
|
clientVars.userName = val;
|
|
},
|
|
},
|
|
{
|
|
name: 'userColor',
|
|
checkVal: null,
|
|
callback: (val) => {
|
|
if (!val || val === 'false') return;
|
|
settings.globalUserColor = val;
|
|
clientVars.userColor = val;
|
|
},
|
|
},
|
|
{
|
|
name: 'rtl',
|
|
checkVal: null,
|
|
callback: (val, fromUrl) => {
|
|
settings.rtlIsTrue = val === 'true';
|
|
if (fromUrl) settings.rtlIsExplicit = true;
|
|
},
|
|
},
|
|
{
|
|
name: 'alwaysShowChat',
|
|
checkVal: 'true',
|
|
callback: (val) => {
|
|
if (!settings.hideChat) chat.stickToScreen();
|
|
},
|
|
},
|
|
{
|
|
name: 'chatAndUsers',
|
|
checkVal: 'true',
|
|
callback: (val) => {
|
|
chat.chatAndUsers();
|
|
},
|
|
},
|
|
{
|
|
name: 'lang',
|
|
checkVal: null,
|
|
callback: (val) => {
|
|
console.log('Val is', val)
|
|
html10n.localize([val, 'en']);
|
|
const prefix = (window as any).clientVars?.cookiePrefix || '';
|
|
Cookies.set(`${prefix}language`, val);
|
|
},
|
|
},
|
|
];
|
|
|
|
const getParams = () => {
|
|
const params = getUrlVars();
|
|
|
|
for (const setting of getParameters) {
|
|
// URL query params take priority over server-enforced options.
|
|
// This prevents race conditions where both fire async callbacks
|
|
// (e.g., lang setting triggers html10n.localize twice).
|
|
const urlValue = params.get(setting.name);
|
|
if (urlValue && (urlValue === setting.checkVal || setting.checkVal == null)) {
|
|
setting.callback(urlValue, true);
|
|
continue;
|
|
}
|
|
|
|
// Fall back to server-enforced option
|
|
let serverValue = clientVars.padOptions[setting.name];
|
|
if (serverValue == null) continue;
|
|
serverValue = serverValue.toString();
|
|
if (serverValue === setting.checkVal || setting.checkVal == null) {
|
|
setting.callback(serverValue, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getUrlVars = () => new URL(window.location.href).searchParams;
|
|
|
|
const getCookieLanguage = () => {
|
|
const cp = (window as any).clientVars?.cookiePrefix || '';
|
|
return Cookies.get(`${cp}language`) || Cookies.get('language');
|
|
};
|
|
|
|
const getMyViewOverrides = () => {
|
|
const language = getCookieLanguage();
|
|
const overrides = {
|
|
showChat: padcookie.getPref('showChat'),
|
|
alwaysShowChat: padcookie.getPref('chatAlwaysVisible'),
|
|
chatAndUsers: padcookie.getPref('chatAndUsers'),
|
|
lang: language,
|
|
view: {
|
|
showAuthorColors: padcookie.getPref('showAuthorshipColors'),
|
|
showLineNumbers: padcookie.getPref('showLineNumbers'),
|
|
rtlIsTrue: padcookie.getPref('rtlIsTrue'),
|
|
padFontFamily: padcookie.getPref('padFontFamily'),
|
|
fadeInactiveAuthorColors: padcookie.getPref('fadeInactiveAuthorColors'),
|
|
},
|
|
};
|
|
if (language == null) delete overrides.lang;
|
|
return overrides;
|
|
};
|
|
|
|
const normalizeChatOptions = (options) => {
|
|
if (options.showChat === false) {
|
|
options.alwaysShowChat = false;
|
|
options.chatAndUsers = false;
|
|
}
|
|
if (options.chatAndUsers === true) {
|
|
options.showChat = true;
|
|
options.alwaysShowChat = true;
|
|
} else if (options.alwaysShowChat === true) {
|
|
options.showChat = true;
|
|
}
|
|
return options;
|
|
};
|
|
|
|
// Surfaces the one-time pad deletion token when the server sends it in
|
|
// clientVars (creator session, first CLIENT_READY). The token is cleared from
|
|
// clientVars on acknowledgement so it is not re-exposed to later code paths.
|
|
const showDeletionTokenModalIfPresent = () => {
|
|
const token: string | null = (window as any).clientVars?.padDeletionToken;
|
|
if (!token) return;
|
|
const $modal = $('#deletiontoken-modal');
|
|
const $input = $('#deletiontoken-value');
|
|
const $copy = $('#deletiontoken-copy');
|
|
const $ack = $('#deletiontoken-ack');
|
|
if ($modal.length === 0) return;
|
|
|
|
$input.val(token);
|
|
const previouslyFocused = document.activeElement as HTMLElement | null;
|
|
$modal.prop('hidden', false).addClass('popup-show');
|
|
// Focus the token input so screen readers announce the dialog body and the
|
|
// user lands on the value they need to copy.
|
|
setTimeout(() => ($input[0] as HTMLInputElement)?.focus(), 0);
|
|
|
|
$copy.off('click.gdpr').on('click.gdpr', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(token);
|
|
} catch (_e) {
|
|
($input[0] as HTMLInputElement).select();
|
|
document.execCommand('copy');
|
|
}
|
|
$copy.text(html10n.get('pad.deletionToken.copied'));
|
|
});
|
|
|
|
$ack.off('click.gdpr').on('click.gdpr', () => {
|
|
$input.val('');
|
|
$modal.prop('hidden', true).removeClass('popup-show');
|
|
(window as any).clientVars.padDeletionToken = null;
|
|
if (previouslyFocused && document.body.contains(previouslyFocused)) {
|
|
previouslyFocused.focus();
|
|
}
|
|
});
|
|
};
|
|
|
|
const sendClientReady = (isReconnect) => {
|
|
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
|
|
// unescape necessary due to Safari and Opera interpretation of spaces
|
|
padId = decodeURIComponent(padId);
|
|
|
|
if (!isReconnect) {
|
|
const titleArray = document.title.split('|');
|
|
const title = titleArray[titleArray.length - 1];
|
|
document.title = `${padId.replace(/_+/g, ' ')} | ${title}`;
|
|
}
|
|
|
|
// The author token lives in an HttpOnly cookie set by the server (GDPR PR3 /
|
|
// ether/etherpad#6701). The integrator-set `sessionID` cookie can also be
|
|
// HttpOnly now (issue #7045). The browser never reads or writes either; the
|
|
// server reads them from the socket.io handshake inside handleClientReady.
|
|
|
|
// If known, propagate the display name and color to the server in the CLIENT_READY message. This
|
|
// allows the server to include the values in its reply CLIENT_VARS message (which avoids
|
|
// initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the
|
|
// pad (which enables them to display a user join notification with the correct name).
|
|
const params = getUrlVars();
|
|
const userInfo = {
|
|
colorId: params.get('userColor'),
|
|
name: params.get('userName'),
|
|
};
|
|
|
|
// The integrator-set `sessionID` cookie is read server-side from the
|
|
// socket.io handshake (issue #7045) so it can be HttpOnly. We no longer
|
|
// forward it via the CLIENT_READY payload.
|
|
const msg: any = {
|
|
component: 'pad',
|
|
type: 'CLIENT_READY',
|
|
padId,
|
|
userInfo,
|
|
};
|
|
const overrides = getMyViewOverrides();
|
|
const viewOverrides = Object.fromEntries(
|
|
Object.entries(overrides.view || {}).filter(([, v]) => v != null));
|
|
const hasTopLevelOverrides = ['showChat', 'alwaysShowChat', 'chatAndUsers', 'lang']
|
|
.some((k) => overrides[k] != null);
|
|
if (Object.keys(viewOverrides).length > 0 || hasTopLevelOverrides) {
|
|
if (Object.keys(viewOverrides).length > 0) overrides.view = viewOverrides;
|
|
else delete overrides.view;
|
|
msg.padSettingsDefaults = overrides;
|
|
}
|
|
|
|
// this is a reconnect, lets tell the server our revisionnumber
|
|
if (isReconnect) {
|
|
msg.client_rev = pad.collabClient.getCurrentRevisionNumber();
|
|
msg.reconnect = true;
|
|
}
|
|
|
|
socket.emit("message", msg);
|
|
};
|
|
|
|
const handshake = async () => {
|
|
let receivedClientVars = false;
|
|
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
|
|
// unescape necessary due to Safari and Opera interpretation of spaces
|
|
padId = decodeURIComponent(padId);
|
|
|
|
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
|
|
// to the proxy/gateway/whatever that this is a pad connection and should be treated as such
|
|
socket = pad.socket = socketio.connect(exports.baseURL, '/', {
|
|
query: {padId},
|
|
reconnectionAttempts: 5,
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 5000,
|
|
});
|
|
|
|
socket.once('connect', () => {
|
|
sendClientReady(false);
|
|
});
|
|
|
|
socket.io.on('reconnect', () => {
|
|
// pad.collabClient might be null if the hanshake failed (or it never got that far).
|
|
if (pad.collabClient != null) {
|
|
pad.collabClient.setChannelState('CONNECTED');
|
|
}
|
|
sendClientReady(receivedClientVars);
|
|
});
|
|
|
|
const socketReconnecting = () => {
|
|
// pad.collabClient might be null if the hanshake failed (or it never got that far).
|
|
if (pad.collabClient != null) {
|
|
pad.collabClient.setStateIdle();
|
|
pad.collabClient.setIsPendingRevision(true);
|
|
pad.collabClient.setChannelState('RECONNECTING');
|
|
}
|
|
};
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
// The socket.io client will automatically try to reconnect for all reasons other than "io
|
|
// server disconnect".
|
|
console.log(`Socket disconnected: ${reason}`)
|
|
//if (reason !== 'io server disconnect' || reason !== 'ping timeout') return;
|
|
socketReconnecting();
|
|
});
|
|
|
|
|
|
socket.on('shout', (obj) => {
|
|
if(obj.type === "COLLABROOM") {
|
|
const payload = obj.data.payload;
|
|
const msgObj = payload?.message || {};
|
|
// Pad-deletion denial shouts are surfaced inline by pad_editor.ts as an
|
|
// alert tied to the delete action; suppress the global "Admin message"
|
|
// gritter so the user doesn't see a confusing duplicate.
|
|
if (typeof msgObj.messageKey === 'string'
|
|
&& msgObj.messageKey.startsWith('pad.deletionToken.')) return;
|
|
// Updater drain announcements get their own title and dodge the generic
|
|
// "Admin message" framing so the user knows it's a system event.
|
|
const isUpdate = typeof msgObj.messageKey === 'string'
|
|
&& msgObj.messageKey.startsWith('update.drain.');
|
|
const text = msgObj.messageKey
|
|
? html10n.get(msgObj.messageKey, msgObj.values || {})
|
|
: msgObj.message;
|
|
if (!text) return;
|
|
const date = new Date(payload.timestamp);
|
|
$.gritter.add({
|
|
title: isUpdate ? html10n.get('update.banner.title') : 'Admin message',
|
|
text: '[' + date.toLocaleTimeString() + ']: ' + text,
|
|
sticky: !!msgObj.sticky
|
|
});
|
|
}
|
|
})
|
|
|
|
socket.io.on('reconnect_attempt', socketReconnecting);
|
|
|
|
socket.io.on('reconnect_failed', (error) => {
|
|
// 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');
|
|
} else {
|
|
throw new Error('Reconnect timed out');
|
|
}
|
|
});
|
|
|
|
|
|
socket.on('error', (error) => {
|
|
// pad.collabClient might be null if the error occurred before the hanshake completed.
|
|
if (pad.collabClient != null) {
|
|
pad.collabClient.setStateIdle();
|
|
pad.collabClient.setIsPendingRevision(true);
|
|
}
|
|
// Don't throw an exception. Error events do not indicate problems that are not already
|
|
// addressed by reconnection logic, so throwing an exception each time there's a socket.io error
|
|
// just annoys users and fills logs.
|
|
});
|
|
|
|
socket.on('message', (obj) => {
|
|
// the access was not granted, give the user a message
|
|
if (obj.accessStatus) {
|
|
if (obj.accessStatus === 'deny') {
|
|
$('#loading').hide();
|
|
$('#permissionDenied').show();
|
|
|
|
if (receivedClientVars) {
|
|
// got kicked
|
|
$('#editorcontainer').hide();
|
|
$('#editorloadingbox').show();
|
|
}
|
|
}
|
|
} else if (!receivedClientVars && obj.type === 'CLIENT_VARS') {
|
|
receivedClientVars = true;
|
|
window.clientVars = obj.data;
|
|
if (window.clientVars.sessionRefreshInterval) {
|
|
const ping =
|
|
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
|
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
|
}
|
|
if(window.clientVars.mode === "development") {
|
|
console.warn('Enabling development mode with live update')
|
|
socket.on('liveupdate', ()=>{
|
|
|
|
console.log('Live reload update received')
|
|
location.reload()
|
|
})
|
|
}
|
|
|
|
} else if (obj.disconnect) {
|
|
padconnectionstatus.disconnected(obj.disconnect);
|
|
socket.disconnect();
|
|
|
|
// block user from making any change to the pad
|
|
padeditor.disable();
|
|
padeditbar.disable();
|
|
padimpexp.disable();
|
|
|
|
return;
|
|
} else {
|
|
pad._messageQ.enqueue(obj);
|
|
}
|
|
});
|
|
|
|
await Promise.all([
|
|
new Promise((resolve) => {
|
|
const h = (obj) => {
|
|
if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return;
|
|
socket.off('message', h);
|
|
resolve();
|
|
};
|
|
socket.on('message', h);
|
|
}),
|
|
// This hook is only intended to be used by test code. If a plugin would like to use this hook,
|
|
// the hook must first be promoted to officially supported by deleting the leading underscore
|
|
// from the name, adding documentation to `doc/api/hooks_client-side.md`, and deleting this
|
|
// comment.
|
|
hooks.aCallAll('_socketCreated', {socket}),
|
|
]);
|
|
};
|
|
|
|
/** Defers message handling until setCollabClient() is called with a non-null value. */
|
|
class MessageQueue {
|
|
constructor() {
|
|
this._q = [];
|
|
this._cc = null;
|
|
}
|
|
|
|
setCollabClient(cc) {
|
|
this._cc = cc;
|
|
this.enqueue(); // Flush.
|
|
}
|
|
|
|
enqueue(...msgs) {
|
|
if (this._cc == null) {
|
|
this._q.push(...msgs);
|
|
} else {
|
|
while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift());
|
|
for (const msg of msgs) this._cc.handleMessageFromServer(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
const pad = {
|
|
// don't access these directly from outside this file, except
|
|
// for debugging
|
|
collabClient: null,
|
|
myUserInfo: null,
|
|
diagnosticInfo: {},
|
|
initTime: 0,
|
|
clientTimeOffset: null,
|
|
padOptions: {},
|
|
_messageQ: new MessageQueue(),
|
|
|
|
// these don't require init; clientVars should all go through here
|
|
getPadId: () => clientVars.padId,
|
|
// Retained as a plugin-compat shim. The server no longer populates
|
|
// clientIp on clientVars (value was always '127.0.0.1'; see #6701 /
|
|
// privacy audit). pad_utils.uniqueId still consumes this as a prefix.
|
|
getClientIp: () => '127.0.0.1',
|
|
getColorPalette: () => clientVars.colorPalette,
|
|
getPrivilege: (name) => clientVars.accountPrivs[name],
|
|
canEditPadSettings: () => !!clientVars.canEditPadSettings,
|
|
getUserId: () => pad.myUserInfo.userId,
|
|
getUserName: () => pad.myUserInfo.name,
|
|
userList: () => paduserlist.users(),
|
|
isPadSettingsEnforcedForMe: () => !!pad.padOptions.enforceSettings && !pad.canEditPadSettings(),
|
|
sendClientMessage: (msg) => {
|
|
pad.collabClient.sendClientMessage(msg);
|
|
},
|
|
getEffectivePadOptions: () => {
|
|
const effectiveOptions = $.extend(true, {}, pad.padOptions);
|
|
if (pad.isPadSettingsEnforcedForMe()) return normalizeChatOptions(effectiveOptions);
|
|
const overrides = getMyViewOverrides();
|
|
for (const key of ['showChat', 'alwaysShowChat', 'chatAndUsers', 'lang']) {
|
|
if (overrides[key] != null) effectiveOptions[key] = overrides[key];
|
|
}
|
|
if (!effectiveOptions.view) effectiveOptions.view = {};
|
|
for (const [key, value] of Object.entries(overrides.view)) {
|
|
if (value != null) effectiveOptions.view[key] = value;
|
|
}
|
|
return normalizeChatOptions(effectiveOptions);
|
|
},
|
|
refreshPadSettingsControls: () => {
|
|
const padOptions = normalizeChatOptions($.extend(true, {}, pad.padOptions || {}));
|
|
const view = padOptions.view || {};
|
|
$('#padsettings-options-disablechat').prop('checked', padOptions.showChat === false);
|
|
$('#padsettings-options-stickychat').prop('checked', !!padOptions.alwaysShowChat);
|
|
$('#padsettings-options-chatandusers').prop('checked', !!padOptions.chatAndUsers);
|
|
$('#padsettings-options-colorscheck').prop('checked', view.showAuthorColors !== false);
|
|
$('#padsettings-options-fadeauthorcheck')
|
|
.prop('checked', view.fadeInactiveAuthorColors !== false);
|
|
$('#padsettings-options-linenoscheck').prop('checked', view.showLineNumbers !== false);
|
|
$('#padsettings-options-rtlcheck').prop('checked', !!view.rtlIsTrue);
|
|
$('#padsettings-viewfontmenu').val(view.padFontFamily || '');
|
|
$('#padsettings-languagemenu').val(padOptions.lang || 'en');
|
|
$('#padsettings-enforcecheck').prop('checked', !!padOptions.enforceSettings);
|
|
$('#padsettings-options-stickychat, #padsettings-options-chatandusers')
|
|
.prop('disabled', padOptions.showChat === false);
|
|
if ($('select').niceSelect) $('select').niceSelect('update');
|
|
},
|
|
refreshMyViewControls: () => {
|
|
const effectiveOptions = pad.getEffectivePadOptions();
|
|
const disabled = pad.isPadSettingsEnforcedForMe();
|
|
$('#options-disablechat').prop('checked', effectiveOptions.showChat === false);
|
|
$('#options-stickychat').prop('checked', !!effectiveOptions.alwaysShowChat);
|
|
$('#options-chatandusers').prop('checked', !!effectiveOptions.chatAndUsers);
|
|
$('#options-colorscheck').prop('checked', effectiveOptions.view?.showAuthorColors !== false);
|
|
$('#options-fadeauthorcheck')
|
|
.prop('checked', effectiveOptions.view?.fadeInactiveAuthorColors !== false);
|
|
$('#options-linenoscheck').prop('checked', effectiveOptions.view?.showLineNumbers !== false);
|
|
$('#options-rtlcheck').prop('checked', !!effectiveOptions.view?.rtlIsTrue);
|
|
$('#viewfontmenu').val(effectiveOptions.view?.padFontFamily || '');
|
|
$('#languagemenu').val(effectiveOptions.lang || 'en');
|
|
$('#settings input[id^="options-"]').prop('disabled', disabled);
|
|
$('#viewfontmenu, #languagemenu').prop('disabled', disabled);
|
|
$('#options-stickychat, #options-chatandusers')
|
|
.prop('disabled', disabled || effectiveOptions.showChat === false);
|
|
$('#enforce-settings-notice').prop('hidden', !disabled);
|
|
if ($('select').niceSelect) $('select').niceSelect('update');
|
|
},
|
|
setMyViewOption: (key, value) => {
|
|
switch (key) {
|
|
case 'showChat':
|
|
padcookie.setPref('showChat', value);
|
|
if (!value) {
|
|
padcookie.setPref('chatAlwaysVisible', false);
|
|
padcookie.setPref('chatAndUsers', false);
|
|
}
|
|
break;
|
|
case 'alwaysShowChat':
|
|
padcookie.setPref('chatAlwaysVisible', value);
|
|
if (value) padcookie.setPref('showChat', true);
|
|
break;
|
|
case 'chatAndUsers':
|
|
padcookie.setPref('chatAndUsers', value);
|
|
if (value) padcookie.setPref('chatAlwaysVisible', true);
|
|
if (value) padcookie.setPref('showChat', true);
|
|
break;
|
|
case 'showAuthorColors':
|
|
padcookie.setPref('showAuthorshipColors', value);
|
|
break;
|
|
default:
|
|
padcookie.setPref(key, value);
|
|
break;
|
|
}
|
|
pad.refreshMyViewControls();
|
|
pad.applyOptionsChange();
|
|
},
|
|
setMyViewLanguage: (lang) => {
|
|
const cp = (window as any).clientVars?.cookiePrefix || '';
|
|
Cookies.set(`${cp}language`, lang);
|
|
pad.refreshMyViewControls();
|
|
pad.applyOptionsChange();
|
|
},
|
|
applyShowChat: (enabled) => {
|
|
settings.hideChat = !enabled;
|
|
if (enabled) {
|
|
if (!window.clientVars.readonly) $('#chaticon').show();
|
|
} else {
|
|
$('#users, .sticky-container').removeClass('chatAndUsers popup-show stickyUsers');
|
|
$('#chatbox').removeClass('chatAndUsersChat stickyChat visible').hide();
|
|
$('#options-stickychat, #options-chatandusers').prop('checked', false);
|
|
$('#chaticon').hide();
|
|
}
|
|
},
|
|
applyStickyChat: (enabled) => {
|
|
const isSticky = $('#chatbox').hasClass('stickyChat');
|
|
$('#options-stickychat').prop('checked', enabled);
|
|
if (enabled !== isSticky) chat.stickToScreen(enabled, false);
|
|
if (!enabled) $('#options-stickychat').prop('disabled', false);
|
|
},
|
|
applyChatAndUsers: (enabled) => {
|
|
const isEnabled = $('#users').hasClass('chatAndUsers');
|
|
$('#options-chatandusers').prop('checked', enabled);
|
|
if (enabled !== isEnabled) chat.chatAndUsers(enabled, false);
|
|
if (!enabled) $('#options-stickychat').prop('disabled', false);
|
|
},
|
|
applyLanguage: (lang) => {
|
|
html10n.localize([lang, 'en']);
|
|
$('#languagemenu').val(lang);
|
|
if ($('select').niceSelect) $('select').niceSelect('update');
|
|
},
|
|
|
|
init() {
|
|
padutils.setupGlobalExceptionHandler();
|
|
|
|
// $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is
|
|
// an async function for some bizarre reason, so the async function is wrapped in a non-async
|
|
// function.
|
|
$(() => (async () => {
|
|
if (window.customStart != null) window.customStart();
|
|
$('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220});
|
|
$('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); });
|
|
padcookie.init();
|
|
padMode.init();
|
|
await handshake();
|
|
this._afterHandshake();
|
|
})());
|
|
},
|
|
_afterHandshake() {
|
|
pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp;
|
|
// initialize the chat
|
|
chat.init(this);
|
|
getParams();
|
|
|
|
padcookie.init(); // initialize the cookies
|
|
pad.initTime = +(new Date());
|
|
pad.padOptions = clientVars.initialOptions;
|
|
|
|
pad.myUserInfo = {
|
|
userId: clientVars.userId,
|
|
name: clientVars.userName,
|
|
ip: pad.getClientIp(),
|
|
colorId: clientVars.userColor,
|
|
};
|
|
|
|
const postAceInit = () => {
|
|
padeditbar.init();
|
|
// Skip link (a11y, ether/etherpad#7255): href="#editorcontainer" gives
|
|
// a working no-JS fallback, but the real focus target is the inner
|
|
// contenteditable inside two nested iframes — route through ace_focus.
|
|
$('#skip-to-content').on('click', (e) => {
|
|
e.preventDefault();
|
|
padeditor.ace.focus();
|
|
});
|
|
// Auto-focusing the editor on load traps Tab inside the editor iframe
|
|
// (Tab inserts an indent there, not bubbling out), which makes the
|
|
// skip link above unreachable via Tab from the URL bar — i.e., the
|
|
// standard WCAG 2.4.1 entry path. Users now click or Tab into the
|
|
// editor; the skip link is the first tabbable element.
|
|
pad.refreshPadSettingsControls();
|
|
pad.applyOptionsChange();
|
|
pad.refreshMyViewControls();
|
|
// The following view overrides MUST run inside postAceInit (after
|
|
// padeditor.init resolves), not in the synchronous tail of
|
|
// _afterHandshake. Running them sync queues a setProperty in the
|
|
// Ace2Editor pending-init queue; the queue is flushed when ace
|
|
// finishes loading, but padeditor.init's own
|
|
// setViewOptions(initialViewOptions) call runs immediately *after*
|
|
// that flush and clobbers the URL-driven value. See #7464 for the
|
|
// RTL incarnation of this race (now generalised here for #7840).
|
|
if (settings.rtlIsExplicit) {
|
|
// URL or server config explicitly set RTL — takes priority over cookie
|
|
pad.changeViewOption('rtlIsTrue', settings.rtlIsTrue === true);
|
|
}
|
|
if (settings.LineNumbersDisabled === true) {
|
|
pad.changeViewOption('showLineNumbers', false);
|
|
}
|
|
if (settings.noColors === true) {
|
|
pad.changeViewOption('noColors', true);
|
|
}
|
|
if (settings.useMonospaceFontGlobal === true) {
|
|
pad.changeViewOption('padFontFamily', 'RobotoMono');
|
|
}
|
|
|
|
// Prevent sticky chat or chat and users to be checked for mobiles
|
|
const checkChatAndUsersVisibility = (x) => {
|
|
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
|
|
setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load
|
|
|
|
$('#editorcontainer').addClass('initialized');
|
|
|
|
if (window.location.hash.toLowerCase() !== '#skinvariantsbuilder' && window.clientVars.enableDarkMode && (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) && !skinVariants.isWhiteModeEnabledInLocalStorage()) {
|
|
skinVariants.updateSkinVariantsClasses(['super-dark-editor', 'dark-background', 'super-dark-toolbar']);
|
|
}
|
|
if (window.clientVars.enableDarkMode) {
|
|
$('#theme-toggle-row').prop('hidden', false);
|
|
$('#options-darkmode').prop('checked', skinVariants.isDarkMode());
|
|
}
|
|
|
|
showDeletionTokenModalIfPresent();
|
|
showPrivacyBannerIfEnabled((clientVars as any).privacyBanner);
|
|
void maybeShowOutdatedNotice();
|
|
|
|
hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars, pad});
|
|
};
|
|
|
|
// order of inits is important here:
|
|
padimpexp.init(this);
|
|
padsavedrevs.init(this);
|
|
padeditor.init(pad.getEffectivePadOptions().view || {}, this).then(postAceInit);
|
|
paduserlist.init(pad.myUserInfo, this);
|
|
padconnectionstatus.init();
|
|
padmodals.init(this);
|
|
|
|
pad.collabClient = getCollabClient(
|
|
padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo,
|
|
{colorPalette: pad.getColorPalette()}, pad);
|
|
this._messageQ.setCollabClient(this.collabClient);
|
|
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
|
|
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
|
|
pad.collabClient.setOnUserLeave(pad.handleUserLeave);
|
|
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
|
|
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
|
|
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
|
|
|
|
// load initial chat-messages
|
|
if (clientVars.chatHead !== -1) {
|
|
const chatHead = clientVars.chatHead;
|
|
const start = Math.max(chatHead - 100, 0);
|
|
pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end: chatHead});
|
|
} else {
|
|
// there are no messages
|
|
$('#chatloadmessagesbutton').css('display', 'none');
|
|
}
|
|
|
|
if (window.clientVars.readonly) {
|
|
chat.hide();
|
|
$('#myusernameedit').attr('disabled', true);
|
|
$('#chatinput').attr('disabled', true);
|
|
$('#chaticon').hide();
|
|
$('#options-chatandusers').parent().hide();
|
|
$('#options-stickychat').parent().hide();
|
|
// The right-side toolbar stays visible on readonly pads. The
|
|
// server-side `toolbar.menu(buttons, isReadOnly)` (see
|
|
// src/node/utils/toolbar.ts) already strips `savedrevision`, and
|
|
// `.readonly .acl-write { display: none }` hides the Import column
|
|
// inside the import/export popup, so the remaining controls
|
|
// (export, timeslider, settings, embed, home, showusers) are all
|
|
// safe for readonly viewers — and the userlist is the surface that
|
|
// plugins like ep_guest hang their "Log In" button off, so hiding
|
|
// it traps guests in readonly with no way out. Iframe-embed use
|
|
// cases that want a clean look (issue #5182) opt in to the hide
|
|
// via `?showMenuRight=false`, or hide the whole editbar via
|
|
// `?showControls=false`.
|
|
} else if (!settings.hideChat) { $('#chaticon').show(); }
|
|
|
|
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');
|
|
|
|
padeditor.ace.callWithAce((ace) => {
|
|
ace.ace_setEditable(!window.clientVars.readonly);
|
|
});
|
|
|
|
// 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 (settings.globalUserColor !== false && colorutils.isCssHex(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
|
|
paduserlist.setMyUserInfo(this.myUserInfo);
|
|
}
|
|
},
|
|
|
|
dispose: () => {
|
|
padeditor.dispose();
|
|
},
|
|
notifyChangeName: (newName) => {
|
|
pad.myUserInfo.name = newName;
|
|
pad.collabClient.updateUserInfo(pad.myUserInfo);
|
|
},
|
|
notifyChangeColor: (newColorId) => {
|
|
pad.myUserInfo.colorId = newColorId;
|
|
pad.collabClient.updateUserInfo(pad.myUserInfo);
|
|
},
|
|
changePadOption: (key, value) => {
|
|
const options = {};
|
|
options[key] = value;
|
|
pad.applyPadSettings(options);
|
|
pad.collabClient.sendClientMessage(
|
|
{
|
|
type: 'padoptions',
|
|
options,
|
|
changedBy: pad.myUserInfo.name || 'unnamed',
|
|
});
|
|
},
|
|
changePadViewOption: (key, value) => {
|
|
const options = {
|
|
view: {},
|
|
};
|
|
options.view[key] = value;
|
|
pad.applyPadSettings(options);
|
|
pad.collabClient.sendClientMessage(
|
|
{
|
|
type: 'padoptions',
|
|
options,
|
|
changedBy: pad.myUserInfo.name || 'unnamed',
|
|
});
|
|
},
|
|
changeViewOption: (key, value) => {
|
|
const effectiveOptions = pad.getEffectivePadOptions();
|
|
if (!effectiveOptions.view) effectiveOptions.view = {};
|
|
effectiveOptions.view[key] = value;
|
|
padeditor.setViewOptions(effectiveOptions.view);
|
|
},
|
|
applyPadSettings: (opts = {}) => {
|
|
// opts object is a full set of options or just
|
|
// some options to change
|
|
for (const key of ['enforceSettings', 'showChat', 'alwaysShowChat', 'chatAndUsers', 'lang']) {
|
|
if (opts[key] == null) continue;
|
|
pad.padOptions[key] = key === 'lang' ? opts[key] : `${opts[key]}` === 'true';
|
|
}
|
|
if (opts.view) {
|
|
if (!pad.padOptions.view) {
|
|
pad.padOptions.view = {};
|
|
}
|
|
for (const [k, v] of Object.entries(opts.view)) {
|
|
pad.padOptions.view[k] = v;
|
|
}
|
|
}
|
|
// Plugin-namespaced keys (ep_*) are passed through verbatim so plugins
|
|
// can ride the existing padoptions broadcast/persist rail. Gated on
|
|
// settings.enablePluginPadOptions (mirrored to clientVars by
|
|
// getPublicSettings). Server-side normalizePadSettings preserves the
|
|
// same keys symmetrically.
|
|
if (clientVars.enablePluginPadOptions) {
|
|
for (const [k, v] of Object.entries(opts)) {
|
|
if (/^ep_[a-z0-9_]+$/.test(k)) pad.padOptions[k] = v;
|
|
}
|
|
}
|
|
normalizeChatOptions(pad.padOptions);
|
|
pad.refreshPadSettingsControls();
|
|
pad.applyOptionsChange();
|
|
},
|
|
applyOptionsChange: () => {
|
|
const effectiveOptions = pad.getEffectivePadOptions();
|
|
padeditor.setViewOptions(effectiveOptions.view || {});
|
|
pad.applyShowChat(effectiveOptions.showChat !== false);
|
|
if (effectiveOptions.showChat !== false) {
|
|
if (effectiveOptions.lang) pad.applyLanguage(effectiveOptions.lang);
|
|
pad.applyChatAndUsers(!!effectiveOptions.chatAndUsers);
|
|
if (!effectiveOptions.chatAndUsers) pad.applyStickyChat(!!effectiveOptions.alwaysShowChat);
|
|
}
|
|
pad.refreshMyViewControls();
|
|
},
|
|
handleOptionsChange: (opts) => {
|
|
pad.applyPadSettings(opts);
|
|
},
|
|
// caller shouldn't mutate the object
|
|
getPadOptions: () => pad.padOptions,
|
|
suggestUserName: (userId, name) => {
|
|
pad.collabClient.sendClientMessage(
|
|
{
|
|
type: 'suggestUserName',
|
|
unnamedId: userId,
|
|
newName: name,
|
|
});
|
|
},
|
|
handleUserJoin: (userInfo) => {
|
|
paduserlist.userJoinOrUpdate(userInfo);
|
|
},
|
|
handleUserUpdate: (userInfo) => {
|
|
paduserlist.userJoinOrUpdate(userInfo);
|
|
},
|
|
handleUserLeave: (userInfo) => {
|
|
paduserlist.userLeave(userInfo);
|
|
},
|
|
handleClientMessage: (msg) => {
|
|
if (msg.type === 'suggestUserName') {
|
|
if (msg.unnamedId === pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) {
|
|
pad.notifyChangeName(msg.newName);
|
|
paduserlist.setMyUserInfo(pad.myUserInfo);
|
|
}
|
|
} else if (msg.type === 'newRevisionList') {
|
|
padsavedrevs.newRevisionList(msg.revisionList);
|
|
} else if (msg.type === 'revisionLabel') {
|
|
padsavedrevs.newRevisionList(msg.revisionList);
|
|
} else if (msg.type === 'padoptions') {
|
|
const opts = msg.options;
|
|
pad.handleOptionsChange(opts);
|
|
}
|
|
},
|
|
showUnacceptedCommitWarning: () => {
|
|
$.gritter.add({
|
|
title: html10n.get('pad.gritter.unacceptedCommit.title'),
|
|
text: html10n.get('pad.gritter.unacceptedCommit.text'),
|
|
sticky: true,
|
|
class_name: 'disconnected unsaved-warning',
|
|
});
|
|
},
|
|
handleChannelStateChange: (newState, message) => {
|
|
const oldFullyConnected = !!padconnectionstatus.isFullyConnected();
|
|
const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting');
|
|
if (newState === 'CONNECTED') {
|
|
padeditor.enable();
|
|
padeditbar.enable();
|
|
padimpexp.enable();
|
|
padconnectionstatus.connected();
|
|
} else if (newState === 'RECONNECTING') {
|
|
padeditor.disable();
|
|
padeditbar.disable();
|
|
padimpexp.disable();
|
|
padconnectionstatus.reconnecting();
|
|
} else if (newState === 'DISCONNECTED') {
|
|
pad.diagnosticInfo.disconnectedMessage = message;
|
|
pad.diagnosticInfo.padId = pad.getPadId();
|
|
pad.diagnosticInfo.socket = {};
|
|
|
|
// 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;
|
|
|
|
if (type === 'string' || type === 'number') {
|
|
pad.diagnosticInfo.socket[i] = value;
|
|
}
|
|
}
|
|
|
|
pad.asyncSendDiagnosticInfo();
|
|
if (typeof window.ajlog === 'string') {
|
|
window.ajlog += (`Disconnected: ${message}\n`);
|
|
}
|
|
padeditor.disable();
|
|
padeditbar.disable();
|
|
padimpexp.disable();
|
|
|
|
padconnectionstatus.disconnected(message);
|
|
if (pad.collabClient.hasUnacceptedCommit()) pad.showUnacceptedCommitWarning();
|
|
}
|
|
const newFullyConnected = !!padconnectionstatus.isFullyConnected();
|
|
if (newFullyConnected !== oldFullyConnected) {
|
|
pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
|
|
}
|
|
},
|
|
handleIsFullyConnected: (isConnected, isInitialConnect) => {
|
|
pad.refreshMyViewControls();
|
|
setTimeout(() => {
|
|
padeditbar.toggleDropDown('none');
|
|
}, 1000);
|
|
},
|
|
determineChatVisibility: (asNowConnectedFeedback) => {
|
|
pad.refreshMyViewControls();
|
|
},
|
|
determineChatAndUsersVisibility: (asNowConnectedFeedback) => {
|
|
pad.refreshMyViewControls();
|
|
},
|
|
determineAuthorshipColorsVisibility: () => {
|
|
pad.refreshMyViewControls();
|
|
},
|
|
handleCollabAction: (action) => {
|
|
if (action === 'commitPerformed') {
|
|
padeditbar.setSyncStatus('syncing');
|
|
} else if (action === 'newlyIdle') {
|
|
padeditbar.setSyncStatus('done');
|
|
}
|
|
},
|
|
asyncSendDiagnosticInfo: () => {
|
|
const currentUrl = window.location.href;
|
|
fetch(`${exports.baseURL}ep/pad/connection-diagnostic-info`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
diagnosticInfo: pad.diagnosticInfo,
|
|
}),
|
|
}).catch((error) => {
|
|
console.error('Error sending diagnostic info:', error);
|
|
})
|
|
},
|
|
forceReconnect: () => {
|
|
$('form#reconnectform input.padId').val(pad.getPadId());
|
|
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()));
|
|
$('form#reconnectform').trigger('submit');
|
|
},
|
|
callWhenNotCommitting: (f) => {
|
|
pad.collabClient.callWhenNotCommitting(f);
|
|
},
|
|
getCollabRevisionNumber: () => pad.collabClient.getCurrentRevisionNumber(),
|
|
isFullyConnected: () => padconnectionstatus.isFullyConnected(),
|
|
addHistoricalAuthors: (data) => {
|
|
if (!pad.collabClient) {
|
|
window.setTimeout(() => {
|
|
pad.addHistoricalAuthors(data);
|
|
}, 1000);
|
|
} else {
|
|
pad.collabClient.addHistoricalAuthors(data);
|
|
}
|
|
},
|
|
};
|
|
|
|
const init = () => pad.init();
|
|
|
|
const settings = {
|
|
LineNumbersDisabled: false,
|
|
noColors: false,
|
|
useMonospaceFontGlobal: false,
|
|
globalUserName: false,
|
|
globalUserColor: false,
|
|
rtlIsTrue: false,
|
|
rtlIsExplicit: false,
|
|
};
|
|
|
|
pad.settings = settings;
|
|
|
|
exports.baseURL = '';
|
|
exports.settings = settings;
|
|
exports.randomString = randomString;
|
|
exports.getParams = getParams;
|
|
exports.pad = pad;
|
|
exports.init = init;
|