mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-10-23 03:37:01 +09:00
Update dependency ckeditor5 to v49 (#4516)
* Update dependency ckeditor5 to v49
* Add editor crash recovery and complete the CKEditor 5 v49 migration
CKEditor 5 v49 removed the Watchdog, so crashed editors are no longer
restarted automatically. The editor wrapper now recreates a crashed
editor from the content synced to its form element (up to 3 times
within a 5s window) and locks it read-only if it keeps crashing, so
the last known-good content is never lost and forms stay submittable.
Also replaces the deprecated ClassicEditor.create() overload with the
config-only form, and drops the dark-mode inline-style hack from the
wysiwyg() Twig function - the refreshed v49 theme pins the editor
colors itself.
* [CodeFactor] Apply fixes to commit b6aa70c
* Address review: reuse editor wrapper and clean up bookkeeping
Recovery now swaps the recreated CKEditor instance into the existing
normalized wrapper via replaceRaw() instead of building a second
wrapper, so references held by callers (element.editor, the resolved
create() promise) stay valid and their change listeners survive.
Editor destruction now unregisters the editor from the page-level
error registry and drops its crash record, instead of retaining both
for the life of the page.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Adam Daley <admdly@users.noreply.github.com>
Co-authored-by: codefactor-io <support@codefactor.io>
This commit is contained in:
7 files changed
+1060
-651
No files matched your search
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Pure helpers for the FOSSBilling editor runtime.
|
||||
*
|
||||
* Kept free of window/document access so they can be unit tested with plain
|
||||
* stubs. See tests/editor-helpers.test.mts.
|
||||
*/
|
||||
|
||||
/** How many times an editor may be recreated for crashes inside the window. */
|
||||
export const EDITOR_CRASH_LIMIT = 3;
|
||||
|
||||
/** Rolling window (ms) inside which repeated crashes count as a crash loop. */
|
||||
export const EDITOR_CRASH_WINDOW_MS = 5000;
|
||||
|
||||
export interface EditorCrashPolicyOptions {
|
||||
limit: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
export interface EditorCrashDecision {
|
||||
times: number[];
|
||||
shouldRecover: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a crash at `now` against previous crash timestamps and decides
|
||||
* whether the editor may be recreated again. Crashes older than the rolling
|
||||
* window are forgotten; once `limit` recoveries have been spent inside the
|
||||
* window, the next crash is refused (the editor is locked instead).
|
||||
*/
|
||||
export function recordEditorCrash(
|
||||
previousTimes: number[],
|
||||
now: number,
|
||||
options: EditorCrashPolicyOptions,
|
||||
): EditorCrashDecision {
|
||||
const times = previousTimes.filter((time) => now - time < options.windowMs);
|
||||
times.push(now);
|
||||
|
||||
return { times, shouldRecover: times.length <= options.limit };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current editor content from the source element. FOSSBilling keeps
|
||||
* the element in sync on every editor change, so this is the last known-good
|
||||
* copy of the content - the safe source for recreating a crashed editor.
|
||||
*/
|
||||
export function readElementData(element) {
|
||||
if ('value' in element) {
|
||||
return element.value;
|
||||
}
|
||||
|
||||
return element.textContent;
|
||||
}
|
||||
|
||||
export function writeElementData(element, data) {
|
||||
if ('value' in element) {
|
||||
element.value = data;
|
||||
} else {
|
||||
element.textContent = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a raw adapter editor behind the stable API used across FOSSBilling
|
||||
* (getData/setData/focus/destroy/onChange/raw).
|
||||
*
|
||||
* Crash recovery recreates the underlying instance; `replaceRaw()` repoints
|
||||
* every method at the new instance and re-attaches change listeners on the
|
||||
* `model.document` path. Adapters that implement their own `onChange()` need
|
||||
* to survive instance swaps on their own.
|
||||
*/
|
||||
export function normalizeEditor(element, editor) {
|
||||
if (!editor || typeof editor.getData !== 'function' || typeof editor.setData !== 'function') {
|
||||
throw new Error('Editor adapters must return getData() and setData() methods.');
|
||||
}
|
||||
|
||||
let current = editor;
|
||||
const changeCallbacks = [];
|
||||
|
||||
const attachChangeListeners = () => {
|
||||
if (current.model?.document?.on) {
|
||||
changeCallbacks.forEach((callback) => {
|
||||
current.model.document.on('change:data', callback);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
attachChangeListeners();
|
||||
|
||||
const normalized = {
|
||||
get raw() {
|
||||
return current.raw || current;
|
||||
},
|
||||
getData: () => current.getData(),
|
||||
setData: (value) => current.setData(value),
|
||||
focus: () => {
|
||||
if (typeof current.focus === 'function') {
|
||||
current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.editing?.view?.focus) {
|
||||
current.editing.view.focus();
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
if (typeof current.destroy === 'function') {
|
||||
return current.destroy();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
onChange: (callback) => {
|
||||
changeCallbacks.push(callback);
|
||||
|
||||
if (typeof current.onChange === 'function') {
|
||||
return current.onChange(callback);
|
||||
}
|
||||
|
||||
if (current.model?.document?.on) {
|
||||
current.model.document.on('change:data', callback);
|
||||
}
|
||||
},
|
||||
replaceRaw(next) {
|
||||
if (!next || next === current) {
|
||||
return;
|
||||
}
|
||||
|
||||
current = next;
|
||||
attachChangeListeners();
|
||||
},
|
||||
element,
|
||||
name: element.getAttribute('name') || element.id || null,
|
||||
required: element.dataset.editorRequired === 'true',
|
||||
};
|
||||
|
||||
return normalized;
|
||||
}
|
||||
+126
-56
@@ -1,5 +1,13 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import { formatCurrencyAmount } from './currency-format.mts';
|
||||
import {
|
||||
EDITOR_CRASH_LIMIT,
|
||||
EDITOR_CRASH_WINDOW_MS,
|
||||
normalizeEditor,
|
||||
readElementData,
|
||||
recordEditorCrash,
|
||||
writeElementData,
|
||||
} from './editor-helpers.mts';
|
||||
|
||||
/**
|
||||
* FOSSBilling browser runtime.
|
||||
@@ -14,6 +22,9 @@ import { formatCurrencyAmount } from './currency-format.mts';
|
||||
const editorsByElement = new WeakMap();
|
||||
const editorsByName = new Map();
|
||||
const adapters = new Map();
|
||||
// Per-element crash-recovery bookkeeping: adapter, options, crash history,
|
||||
// and the unregister function for the adapter's error reporting.
|
||||
const crashRecords = new WeakMap();
|
||||
const cookieNames = Object.freeze({
|
||||
locale: 'fossbilling_locale',
|
||||
timezone: 'fossbilling_timezone',
|
||||
@@ -158,58 +169,129 @@ import { formatCurrencyAmount } from './currency-format.mts';
|
||||
|
||||
FOSSBilling.initTimezone = initTimezone;
|
||||
|
||||
function getEditorName(element) {
|
||||
return element.getAttribute('name') || element.id || null;
|
||||
}
|
||||
|
||||
function syncElementData(element, editor) {
|
||||
const data = editor.getData();
|
||||
if ('value' in element) {
|
||||
element.value = data;
|
||||
} else {
|
||||
element.textContent = data;
|
||||
}
|
||||
writeElementData(element, editor.getData());
|
||||
}
|
||||
|
||||
function normalizeEditor(element, editor) {
|
||||
if (!editor || typeof editor.getData !== 'function' || typeof editor.setData !== 'function') {
|
||||
throw new Error('Editor adapters must return getData() and setData() methods.');
|
||||
function registerEditorInstance(element, adapter, options, rawEditor) {
|
||||
const editor = normalizeEditor(element, rawEditor);
|
||||
// Normal destruction must also drop the crash-recovery bookkeeping, so
|
||||
// the page-level error registry does not retain the editor.
|
||||
const destroy = editor.destroy;
|
||||
editor.destroy = () => {
|
||||
crashRecords.get(element)?.off?.();
|
||||
crashRecords.delete(element);
|
||||
return destroy();
|
||||
};
|
||||
|
||||
syncElementData(element, editor);
|
||||
editor.onChange(() => syncElementData(element, editor));
|
||||
|
||||
editorsByElement.set(element, editor);
|
||||
if (editor.name) {
|
||||
editorsByName.set(editor.name, editor);
|
||||
}
|
||||
|
||||
return {
|
||||
raw: editor.raw || editor,
|
||||
getData: () => editor.getData(),
|
||||
setData: (value) => editor.setData(value),
|
||||
focus: () => {
|
||||
if (typeof editor.focus === 'function') {
|
||||
editor.focus();
|
||||
return;
|
||||
}
|
||||
element.editor = editor;
|
||||
element.setAttribute('data-editor', 'true');
|
||||
|
||||
if (editor.editing?.view?.focus) {
|
||||
editor.editing.view.focus();
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
if (typeof editor.destroy === 'function') {
|
||||
return editor.destroy();
|
||||
}
|
||||
attachCrashRecovery(element, adapter, options, rawEditor);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
onChange: (callback) => {
|
||||
if (typeof editor.onChange === 'function') {
|
||||
return editor.onChange(callback);
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
|
||||
if (editor.model?.document?.on) {
|
||||
editor.model.document.on('change:data', callback);
|
||||
}
|
||||
},
|
||||
element,
|
||||
name: getEditorName(element),
|
||||
required: element.dataset.editorRequired === 'true',
|
||||
function attachCrashRecovery(element, adapter, options, rawEditor) {
|
||||
if (typeof adapter.onFatalError !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = crashRecords.get(element);
|
||||
previous?.off?.();
|
||||
|
||||
const record = {
|
||||
adapter,
|
||||
options,
|
||||
// Crash history survives recreations of the same element; a fresh
|
||||
// registration after a permanent lock starts over.
|
||||
times: previous && !previous.locked ? previous.times : [],
|
||||
recovering: false,
|
||||
locked: false,
|
||||
off: null,
|
||||
};
|
||||
|
||||
record.off = adapter.onFatalError(rawEditor, (error) => {
|
||||
handleFatalError(element, error);
|
||||
});
|
||||
|
||||
crashRecords.set(element, record);
|
||||
}
|
||||
|
||||
function handleFatalError(element, error) {
|
||||
const record = crashRecords.get(element);
|
||||
if (!record || record.recovering || record.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decision = recordEditorCrash(record.times, Date.now(), {
|
||||
limit: EDITOR_CRASH_LIMIT,
|
||||
windowMs: EDITOR_CRASH_WINDOW_MS,
|
||||
});
|
||||
record.times = decision.times;
|
||||
|
||||
if (!decision.shouldRecover) {
|
||||
record.locked = true;
|
||||
record.off?.();
|
||||
record.adapter.lock?.(editorsByElement.get(element)?.raw);
|
||||
console.error(
|
||||
'FOSSBilling editor crashed repeatedly and is now read-only. Its content is kept in the form field - reload the page to get a fresh editor.',
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
record.recovering = true;
|
||||
recreateEditor(element, record)
|
||||
.catch((recoveryError) => {
|
||||
record.locked = true;
|
||||
record.off?.();
|
||||
console.error(
|
||||
'FOSSBilling editor recovery failed. Its last content is kept in the form field - reload the page to get a fresh editor.',
|
||||
recoveryError,
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
record.recovering = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function recreateEditor(element, record) {
|
||||
// Read the content from the synced form element, not editor.getData():
|
||||
// the element holds the last known-good copy of the content.
|
||||
const previous = editorsByElement.get(element);
|
||||
const data = readElementData(element);
|
||||
|
||||
try {
|
||||
await previous?.raw?.destroy?.();
|
||||
} catch (destroyError) {
|
||||
console.error('FOSSBilling editor could not destroy the crashed instance:', destroyError);
|
||||
}
|
||||
|
||||
// The destroyed editor may write its own state back to the element
|
||||
// (updateSourceElementOnDestroy), so restore the captured copy.
|
||||
writeElementData(element, data);
|
||||
|
||||
const rawEditor = await record.adapter.create(element, { ...record.options, initialData: data });
|
||||
|
||||
// Reuse the existing wrapper so references callers hold stay valid;
|
||||
// replaceRaw() re-attaches their change listeners to the new instance.
|
||||
if (previous) {
|
||||
previous.replaceRaw(rawEditor);
|
||||
syncElementData(element, previous);
|
||||
attachCrashRecovery(element, record.adapter, record.options, rawEditor);
|
||||
return previous;
|
||||
}
|
||||
|
||||
return registerEditorInstance(element, record.adapter, record.options, rawEditor);
|
||||
}
|
||||
|
||||
FOSSBilling.editor = Object.assign({
|
||||
@@ -238,20 +320,8 @@ import { formatCurrencyAmount } from './currency-format.mts';
|
||||
}
|
||||
|
||||
const rawEditor = await adapter.create(element, options);
|
||||
const editor = normalizeEditor(element, rawEditor);
|
||||
|
||||
syncElementData(element, editor);
|
||||
editor.onChange(() => syncElementData(element, editor));
|
||||
|
||||
editorsByElement.set(element, editor);
|
||||
if (editor.name) {
|
||||
editorsByName.set(editor.name, editor);
|
||||
}
|
||||
|
||||
element.editor = editor;
|
||||
element.setAttribute('data-editor', 'true');
|
||||
|
||||
return editor;
|
||||
return registerEditorInstance(element, adapter, options, rawEditor);
|
||||
},
|
||||
|
||||
init(selector, options = {}) {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
import {
|
||||
EDITOR_CRASH_LIMIT,
|
||||
EDITOR_CRASH_WINDOW_MS,
|
||||
normalizeEditor,
|
||||
readElementData,
|
||||
recordEditorCrash,
|
||||
writeElementData,
|
||||
} from '../editor-helpers.mts';
|
||||
|
||||
function makeElement(overrides = {}) {
|
||||
return {
|
||||
getAttribute: () => null,
|
||||
id: 'editor-1',
|
||||
dataset: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRawEditor(overrides = {}) {
|
||||
const model = makeModel();
|
||||
return { getData: () => '', setData: () => {}, model, ...overrides };
|
||||
}
|
||||
|
||||
function makeModel() {
|
||||
const listeners = [];
|
||||
return {
|
||||
document: {
|
||||
on: (event, callback) => {
|
||||
listeners.push([event, callback]);
|
||||
},
|
||||
},
|
||||
fire(event) {
|
||||
listeners.filter(([name]) => name === event).forEach(([, callback]) => callback());
|
||||
},
|
||||
listenerCount(event) {
|
||||
return listeners.filter(([name]) => name === event).length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('recordEditorCrash', () => {
|
||||
const policy = { limit: EDITOR_CRASH_LIMIT, windowMs: EDITOR_CRASH_WINDOW_MS };
|
||||
|
||||
test('recovers on the first crash', () => {
|
||||
const decision = recordEditorCrash([], 1000, policy);
|
||||
|
||||
assert.deepEqual(decision.times, [1000]);
|
||||
assert.equal(decision.shouldRecover, true);
|
||||
});
|
||||
|
||||
test('recovers up to the crash limit within the window', () => {
|
||||
let times: number[] = [];
|
||||
for (const now of [1000, 2000, 3000]) {
|
||||
const decision = recordEditorCrash(times, now, policy);
|
||||
times = decision.times;
|
||||
assert.equal(decision.shouldRecover, true);
|
||||
}
|
||||
});
|
||||
|
||||
test('refuses to recover once the limit is spent inside the window', () => {
|
||||
let times: number[] = [];
|
||||
for (const now of [1000, 2000, 3000]) {
|
||||
times = recordEditorCrash(times, now, policy).times;
|
||||
}
|
||||
|
||||
const decision = recordEditorCrash(times, 4000, policy);
|
||||
|
||||
assert.equal(decision.times.length, 4);
|
||||
assert.equal(decision.shouldRecover, false);
|
||||
});
|
||||
|
||||
test('forgets crashes older than the rolling window', () => {
|
||||
const decision = recordEditorCrash([1000, 2000, 3000], 3000 + EDITOR_CRASH_WINDOW_MS, policy);
|
||||
|
||||
assert.deepEqual(decision.times, [3000 + EDITOR_CRASH_WINDOW_MS]);
|
||||
assert.equal(decision.shouldRecover, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readElementData/writeElementData', () => {
|
||||
test('uses the value property for form-field elements', () => {
|
||||
const element = { value: '<p>old</p>' };
|
||||
|
||||
assert.equal(readElementData(element), '<p>old</p>');
|
||||
|
||||
writeElementData(element, '<p>new</p>');
|
||||
assert.equal(element.value, '<p>new</p>');
|
||||
});
|
||||
|
||||
test('falls back to textContent for non-form elements', () => {
|
||||
const element = { textContent: 'old' };
|
||||
|
||||
assert.equal(readElementData(element), 'old');
|
||||
|
||||
writeElementData(element, 'new');
|
||||
assert.equal(element.textContent, 'new');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeEditor', () => {
|
||||
test('rejects editors without getData and setData', () => {
|
||||
assert.throws(() => normalizeEditor(makeElement(), {}), /getData\(\) and setData\(\)/);
|
||||
assert.throws(() => normalizeEditor(makeElement(), null), /getData\(\) and setData\(\)/);
|
||||
});
|
||||
|
||||
test('delegates getData and setData to the current instance', () => {
|
||||
let firstSet;
|
||||
let secondSet;
|
||||
const first = makeRawEditor({ getData: () => 'first', setData: (value) => { firstSet = value; } });
|
||||
const second = makeRawEditor({ getData: () => 'second', setData: (value) => { secondSet = value; } });
|
||||
const editor = normalizeEditor(makeElement(), first);
|
||||
|
||||
assert.equal(editor.getData(), 'first');
|
||||
|
||||
editor.replaceRaw(second);
|
||||
assert.equal(editor.getData(), 'second');
|
||||
|
||||
editor.setData('hello');
|
||||
assert.equal(secondSet, 'hello');
|
||||
assert.equal(firstSet, undefined);
|
||||
});
|
||||
|
||||
test('ignores empty or identical replaceRaw calls', () => {
|
||||
const first = makeRawEditor();
|
||||
const editor = normalizeEditor(makeElement(), first);
|
||||
|
||||
editor.replaceRaw(null);
|
||||
editor.replaceRaw(first);
|
||||
|
||||
assert.equal(editor.raw, first);
|
||||
});
|
||||
|
||||
test('raw exposes editor.raw when the adapter provides one', () => {
|
||||
const inner = { marker: true };
|
||||
const raw = makeRawEditor({ raw: inner });
|
||||
const editor = normalizeEditor(makeElement(), raw);
|
||||
|
||||
assert.equal(editor.raw, inner);
|
||||
});
|
||||
|
||||
test('re-binds change listeners onto the replaced instance', () => {
|
||||
const first = makeRawEditor();
|
||||
const second = makeRawEditor();
|
||||
const editor = normalizeEditor(makeElement(), first);
|
||||
|
||||
const seen = [];
|
||||
editor.onChange(() => seen.push('change'));
|
||||
|
||||
editor.replaceRaw(second);
|
||||
second.model.fire('change:data');
|
||||
|
||||
assert.deepEqual(seen, ['change']);
|
||||
assert.equal(first.model.listenerCount('change:data'), 1);
|
||||
assert.equal(second.model.listenerCount('change:data'), 1);
|
||||
});
|
||||
|
||||
test('delegates onChange to editors that provide their own', () => {
|
||||
const registered = [];
|
||||
const raw = makeRawEditor({
|
||||
onChange: (callback) => registered.push(callback),
|
||||
});
|
||||
const editor = normalizeEditor(makeElement(), raw);
|
||||
|
||||
editor.onChange(() => {});
|
||||
|
||||
assert.equal(registered.length, 1);
|
||||
});
|
||||
|
||||
test('focus prefers editor.focus and falls back to the editing view', () => {
|
||||
let focused = 0;
|
||||
const withFocus = makeRawEditor({ focus: () => { focused += 1; } });
|
||||
normalizeEditor(makeElement(), withFocus).focus();
|
||||
assert.equal(focused, 1);
|
||||
|
||||
let viewFocused = 0;
|
||||
const withView = makeRawEditor({
|
||||
editing: { view: { focus: () => { viewFocused += 1; } } },
|
||||
});
|
||||
normalizeEditor(makeElement(), withView).focus();
|
||||
assert.equal(viewFocused, 1);
|
||||
});
|
||||
|
||||
test('destroy delegates to the instance', async () => {
|
||||
let destroyed = 0;
|
||||
const raw = makeRawEditor({ destroy: () => { destroyed += 1; return Promise.resolve(); } });
|
||||
const editor = normalizeEditor(makeElement(), raw);
|
||||
|
||||
await editor.destroy();
|
||||
|
||||
assert.equal(destroyed, 1);
|
||||
});
|
||||
|
||||
test('destroy resolves when the instance has no destroy method', async () => {
|
||||
const raw = makeRawEditor();
|
||||
const editor = normalizeEditor(makeElement(), raw);
|
||||
|
||||
await assert.doesNotReject(editor.destroy());
|
||||
});
|
||||
|
||||
test('reads the name from the name attribute, then the id', () => {
|
||||
const byName = normalizeEditor(makeElement({ getAttribute: (key) => (key === 'name' ? 'body' : null) }), makeRawEditor());
|
||||
assert.equal(byName.name, 'body');
|
||||
|
||||
const byId = normalizeEditor(makeElement({ id: 'intro' }), makeRawEditor());
|
||||
assert.equal(byId.name, 'intro');
|
||||
});
|
||||
|
||||
test('tracks the required flag from the dataset', () => {
|
||||
assert.equal(normalizeEditor(makeElement(), makeRawEditor()).required, false);
|
||||
assert.equal(
|
||||
normalizeEditor(makeElement({ dataset: { editorRequired: 'true' } }), makeRawEditor()).required,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,6 @@ import '@ckeditor/ckeditor5-typing/dist/index.css';
|
||||
import '@ckeditor/ckeditor5-undo/dist/index.css';
|
||||
import '@ckeditor/ckeditor5-upload/dist/index.css';
|
||||
import '@ckeditor/ckeditor5-utils/dist/index.css';
|
||||
import '@ckeditor/ckeditor5-watchdog/dist/index.css';
|
||||
import '@ckeditor/ckeditor5-widget/dist/index.css';
|
||||
|
||||
import '@ckeditor/ckeditor5-autoformat/dist/index.css';
|
||||
@@ -99,8 +98,28 @@ class FOSSBillingCKEditor extends ClassicEditor {
|
||||
|
||||
window.FOSSBilling.editor.registerAdapter('ckeditor', {
|
||||
create(element, options = {}) {
|
||||
const { adapter, ...editorOptions } = options;
|
||||
const { adapter, initialData, ...editorOptions } = options;
|
||||
|
||||
return FOSSBillingCKEditor.create(element, editorOptions);
|
||||
const config = { ...editorOptions, attachTo: element };
|
||||
if (initialData !== undefined) {
|
||||
config.root = { ...config.root, initialData };
|
||||
}
|
||||
|
||||
return FOSSBillingCKEditor.create(config);
|
||||
},
|
||||
|
||||
// Reports runtime errors that escape the given editor instance so the
|
||||
// wrapper can recover from them (v49 removed the Watchdog).
|
||||
onFatalError(editor, callback) {
|
||||
return FOSSBillingCKEditor.onEditorError(({ error, source }) => {
|
||||
if (source === editor) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Stops further editing while keeping the content visible and submittable.
|
||||
lock(editor) {
|
||||
editor.enableReadOnlyMode('fossbilling-editor-crash');
|
||||
},
|
||||
});
|
||||
Generated
+556
-583
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -33,7 +33,7 @@
|
||||
"@playwright/test": "1.64.1",
|
||||
"@types/node": "24.19.2",
|
||||
"autoprefixer": "10.6.1",
|
||||
"ckeditor5": "48.5.2",
|
||||
"ckeditor5": "49.0.0",
|
||||
"esbuild": "0.28.2",
|
||||
"github-markdown-css": "5.9.0",
|
||||
"postcss": "8.5.30",
|
||||
|
||||
@@ -311,14 +311,6 @@ class FOSSBillingExtension
|
||||
<script>
|
||||
FOSSBilling.ready(function () {
|
||||
FOSSBilling.editor.init({$selectorJson}, {$optionsJson});
|
||||
|
||||
if (document.documentElement.getAttribute('data-bs-theme') === 'dark' || localStorage.getItem('theme') === 'dark') {
|
||||
setTimeout(function () {
|
||||
document.querySelectorAll('.ck-editor__main').forEach(function (element) {
|
||||
element.style.color = '#1d273b';
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
HTML,
|
||||
|
||||
Reference in new issue
Block a user