@ckeditor/ckeditor5-vue
Version:
Official Vue.js 3+ component for CKEditor 5 – the best browser-based rich text editor.
1,188 lines (1,187 loc) • 47.4 kB
JavaScript
import * as Vue from "vue";
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, getCurrentInstance, markRaw, mergeModels, mergeProps, nextTick, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, shallowReadonly, toValue, unref, useModel, version, watch, watchEffect } from "vue";
import { appendExtraPluginsToEditorConfig, assignAttributesPropToMultiRootEditorConfig, assignElementToEditorConfig, assignInitialDataToEditorConfig, assignInitialDataToMultirootEditorConfig, compareInstalledCKBaseVersion, createIntegrationUsageDataPlugin, getInstalledCKBaseFeatures, isCKEditorFreeLicense, loadCKEditorCloud, loadCKEditorCloud as loadCKEditorCloud$1, uid } from "@ckeditor/ckeditor5-integrations-common";
import { debounce } from "lodash-es";
//#region src/plugins/VueIntegrationUsageDataPlugin.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* This part of the code is not executed in open-source implementations using a GPL key.
* It only runs when a specific license key is provided. If you are uncertain whether
* this applies to your installation, please contact our support team.
*/
var VueIntegrationUsageDataPlugin = createIntegrationUsageDataPlugin("vue", {
version: "8.2.0",
frameworkVersion: version
});
/**
* Appends all integration plugins to the editor configuration.
*
* @param editorConfig The editor configuration.
* @returns The editor configuration with all integration plugins appended.
*/
function appendUsageDataPluginToConfig(editorConfig) {
/**
* Do not modify the editor configuration if the editor is using a free license.
*/
if (isCKEditorFreeLicense(editorConfig.licenseKey)) return editorConfig;
return appendExtraPluginsToEditorConfig(editorConfig, [VueIntegrationUsageDataPlugin]);
}
//#endregion
//#region src/utils/cleanupOrphanEditorElements.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Removes all DOM elements injected by a specific CKEditor instance.
* Call this before assigning a new instance (e.g. in the 'restart' watchdog handler),
* because the watchdog does not clean up the previous editor's DOM on its own.
*/
function cleanupOrphanEditorElements(editor) {
var _editor$ui, _editor$ui2, _editor$editing;
const uiElement = (_editor$ui = editor.ui) === null || _editor$ui === void 0 ? void 0 : _editor$ui.element;
if (uiElement === null || uiElement === void 0 ? void 0 : uiElement.isConnected) uiElement.remove();
const bodyCollectionContainer = (_editor$ui2 = editor.ui) === null || _editor$ui2 === void 0 || (_editor$ui2 = _editor$ui2.view) === null || _editor$ui2 === void 0 || (_editor$ui2 = _editor$ui2.body) === null || _editor$ui2 === void 0 ? void 0 : _editor$ui2._bodyCollectionContainer;
if (bodyCollectionContainer === null || bodyCollectionContainer === void 0 ? void 0 : bodyCollectionContainer.isConnected) bodyCollectionContainer.remove();
const editingView = (_editor$editing = editor.editing) === null || _editor$editing === void 0 ? void 0 : _editor$editing.view;
if (editingView) for (const domRoot of editingView.domRoots.values()) {
if (!(domRoot instanceof HTMLElement)) continue;
domRoot.removeAttribute("contenteditable");
domRoot.removeAttribute("role");
domRoot.removeAttribute("aria-label");
domRoot.removeAttribute("aria-multiline");
domRoot.removeAttribute("spellcheck");
domRoot.classList.remove("ck", "ck-content", "ck-editor__editable", "ck-rounded-corners", "ck-editor__editable_inline", "ck-blurred", "ck-focused");
}
}
//#endregion
//#region src/utils/wrapWithWatchdogIfPresent.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
var EDITOR_WATCHDOG_SYMBOL = Symbol.for("vue-editor-watchdog");
/**
* Returns an editor constructor optionally wrapped with EditorWatchdog.
*/
function resolveEditorConstructor(Editor, disableWatchdog, watchdogConfig) {
return disableWatchdog ? Editor : wrapWithWatchdogIfPresent(Editor, watchdogConfig);
}
/**
* `EditorWatchdog#create` method does not return editor instance (returns `undefined` instead).
* This function wraps editor constructor with EditorWatchdog and returns fake constructor that
* returns editor instance assigned to initialized watchdog.
*
* It stores watchdog instance in hidden symbol assigned to editor. It simplifies storing both
* instances in component's state (it's no longer required to store them separately).
*
* @param Editor The Editor creator to wrap.
* @param watchdogConfig Watchdog configuration.
* @returns The Editor creator wrapped with a watchdog.
*/
function wrapWithWatchdogIfPresent(Editor, watchdogConfig) {
const { EditorWatchdog } = Editor;
if (!EditorWatchdog) return Editor;
const watchdog = new EditorWatchdog(Editor, watchdogConfig);
watchdog.setCreator(async (...args) => {
const editor = await Editor.create(...args);
editor[EDITOR_WATCHDOG_SYMBOL] = watchdog;
return editor;
});
return {
...Editor,
editorName: Editor.editorName,
create: async (...args) => {
await watchdog.create(...args);
return watchdog.editor;
}
};
}
/**
* Unwraps the EditorWatchdog from the editor instance.
*
* @param editor Editor with attached watchdog.
*/
function unwrapEditorWatchdog(editor) {
var _editor$EDITOR_WATCHD;
return (_editor$EDITOR_WATCHD = editor[EDITOR_WATCHDOG_SYMBOL]) !== null && _editor$EDITOR_WATCHD !== void 0 ? _editor$EDITOR_WATCHD : null;
}
/**
* Attaches common EditorWatchdog runtime error handling.
*/
function attachEditorWatchdogErrorHandler(editor, { isUnmounted, onError }) {
const watchdog = unwrapEditorWatchdog(editor);
if (!watchdog) return null;
watchdog.on("error", (_, { error, causesRestart }) => {
if (isUnmounted()) return;
onError({
error,
causesRestart,
watchdog,
editor: watchdog.editor
});
});
return watchdog;
}
/**
* It destroys the editor watchdog if it is assigned to the editor. If it is not, the editor is destroyed.
*
* @param editor Editor with attached watchdog.
*/
async function destroyEditorWithWatchdog(editor) {
const watchdog = unwrapEditorWatchdog(editor);
if (watchdog) await watchdog.destroy();
else await editor.destroy();
}
//#endregion
//#region src/composables/useIsUnmounted.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
function useIsUnmounted() {
const isUnmounted = ref(false);
onBeforeUnmount(() => {
isUnmounted.value = true;
});
return isUnmounted;
}
//#endregion
//#region src/composables/useEditorLifecycleEvents.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Hook that watches editor lifecycle events and maps them to Vue event emitters.
*/
function useEditorLifecycleEvents(instance, emit) {
watch(instance, (newInstance) => {
/* istanbul ignore if -- @preserve - Defensive check, instance never becomes undefined. */
if (!newInstance) return;
const { document } = newInstance.editing.view;
document.on("focus", (evt) => emit("focus", evt, newInstance));
document.on("blur", (evt) => emit("blur", evt, newInstance));
emit("ready", newInstance);
newInstance.once("destroy", () => {
emit("destroy", newInstance);
});
}, { flush: "post" });
}
//#endregion
//#region src/composables/useEditorVModel.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
var INPUT_EVENT_DEBOUNCE_WAIT$1 = 300;
/**
* Hook that synchronizes editor state with currently set vue model.
*/
function useEditorVModel({ disableTwoWayDataBinding, emit, instance, model }) {
const lastEditorData = ref();
const isUnmounted = useIsUnmounted();
/**
* Updates the internal cache and emits Vue-compatible events.
*/
function assignEditorDataToModel(editor, evt = null) {
const data = lastEditorData.value = editor.data.get();
emit("update:modelValue", data, evt, editor);
emit("input", data, evt, editor);
}
watch(model, (newModel) => {
if (instance.value && newModel !== lastEditorData.value) instance.value.data.set(newModel);
});
watch(instance, (newInstance, _oldInstance, onCleanup) => {
/* istanbul ignore if -- @preserve - Defensive check, instance never becomes undefined. */
if (!newInstance) return;
const emitDebouncedInputEvent = debounce((evt) => {
if (toValue(disableTwoWayDataBinding) || isUnmounted.value) return;
assignEditorDataToModel(newInstance, evt);
}, INPUT_EVENT_DEBOUNCE_WAIT$1, { leading: true });
newInstance.model.document.on("change:data", emitDebouncedInputEvent);
newInstance.once("destroy", () => {
emitDebouncedInputEvent.cancel();
});
onCleanup(() => {
emitDebouncedInputEvent.cancel();
});
});
return {
lastEditorData,
assignEditorDataToModel
};
}
//#endregion
//#region src/composables/useEditorReadOnly.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
var INTEGRATION_READ_ONLY_LOCK_ID = "Lock from Vue integration (@ckeditor/ckeditor5-vue)";
/**
* Hook that toggles readonly state on provided instance.
*/
function useEditorReadOnly(instance, disabled) {
watchEffect(() => {
const editor = toValue(instance);
const isDisabled = !!toValue(disabled);
if (editor) toggleEditorReadOnly(editor, isDisabled);
}, { flush: "sync" });
}
/**
* Toggles editor to readonly state.
*/
function toggleEditorReadOnly(editor, readOnly) {
if (readOnly) editor.enableReadOnlyMode(INTEGRATION_READ_ONLY_LOCK_ID);
else editor.disableReadOnlyMode(INTEGRATION_READ_ONLY_LOCK_ID);
}
//#endregion
//#region src/composables/useEditorVersionCheck.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Hook that check if integration is compatible with installed version of the editor.
*/
function useEditorVersionCheck() {
switch (compareInstalledCKBaseVersion("42.0.0")) {
case null:
console.warn("Cannot find the \"CKEDITOR_VERSION\" in the \"window\" scope.");
break;
case -1:
console.warn("The <CKEditor> component requires using CKEditor 5 in version 42+ or nightly build.");
break;
}
}
//#endregion
//#region src/utils/isClassicEditor.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
function isClassicEditor(Editor) {
return !Editor.editorName || Editor.editorName === "ClassicEditor";
}
//#endregion
//#region src/composables/useEditorElementDefinition.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Picks editor element definition from config if provided.
*/
function useEditorElementDefinition({ Editor, config, defaultElementName }) {
return computed(() => {
const _config = toValue(config);
if (!isClassicEditor(toValue(Editor))) {
var _config$roots$main$el, _config$roots, _config$root;
const customElementDefinition = (_config$roots$main$el = (_config$roots = _config.roots) === null || _config$roots === void 0 || (_config$roots = _config$roots.main) === null || _config$roots === void 0 ? void 0 : _config$roots.element) !== null && _config$roots$main$el !== void 0 ? _config$roots$main$el : (_config$root = _config.root) === null || _config$root === void 0 ? void 0 : _config$root.element;
if (customElementDefinition) return customElementDefinition;
}
return toValue(defaultElementName);
});
}
//#endregion
//#region src/utils/normalizeEditorElementDefinition.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Normalizes an editor element definition into a structured object.
*
* @param definition The definition to normalize.
* @returns A strictly typed object definition containing at least the element name.
*/
function normalizeEditorElementDefinition(definition) {
if (typeof HTMLElement !== "undefined" && definition instanceof HTMLElement) throw new Error("An HTMLElement cannot be used as an editor element definition. Please pass a string or an object definition.");
if (typeof definition !== "object" || definition === null) return { name: definition };
return definition;
}
//#endregion
//#region src/DynamicElement.vue
var DynamicElement_default = /* @__PURE__ */ defineComponent({
__name: "DynamicElement",
props: { definition: { default: null } },
setup(__props, { expose: __expose }) {
const props = __props;
const elementRef = ref();
__expose({ elementRef });
const definition = computed(() => {
var _props$definition;
return normalizeEditorElementDefinition((_props$definition = props.definition) !== null && _props$definition !== void 0 ? _props$definition : "div");
});
return (_ctx, _cache) => {
return openBlock(), createBlock(resolveDynamicComponent(definition.value.name), mergeProps({
ref_key: "elementRef",
ref: elementRef
}, definition.value.attributes, {
class: definition.value.classes,
style: definition.value.styles
}), null, 16, ["class", "style"]);
};
}
});
//#endregion
//#region src/Ckeditor.vue
var Ckeditor_default = /* @__PURE__ */ defineComponent({
name: "CKEditor",
__name: "Ckeditor",
props: /*@__PURE__*/ mergeModels({
editor: {},
config: { default: () => ({}) },
disabled: {
type: Boolean,
default: false
},
disableTwoWayDataBinding: {
type: Boolean,
default: false
},
watchdogConfig: {},
disableWatchdog: {
type: Boolean,
default: false
},
tagName: { default: "div" }
}, {
"modelValue": {
type: String,
default: ""
},
"modelModifiers": {}
}),
emits: /*@__PURE__*/ mergeModels([
"ready",
"destroy",
"blur",
"focus",
"input",
"update:modelValue",
"error"
], ["update:modelValue"]),
setup(__props, { expose: __expose, emit: __emit }) {
const model = useModel(__props, "modelValue");
const props = __props;
const emit = __emit;
const currentInstance = getCurrentInstance();
const hasErrorHandler = () => {
var _currentInstance$vnod;
return !!(currentInstance === null || currentInstance === void 0 || (_currentInstance$vnod = currentInstance.vnode.props) === null || _currentInstance$vnod === void 0 ? void 0 : _currentInstance$vnod.onError);
};
const editorElementRef = ref();
const instance = ref();
const isUnmounted = useIsUnmounted();
const { lastEditorData, assignEditorDataToModel } = useEditorVModel({
disableTwoWayDataBinding: () => props.disableTwoWayDataBinding,
model,
emit,
instance
});
const elementDefinition = useEditorElementDefinition({
Editor: () => props.editor,
config: () => props.config,
defaultElementName: () => props.tagName
});
useEditorVersionCheck();
useEditorLifecycleEvents(instance, emit);
useEditorReadOnly(instance, () => props.disabled);
__expose({
instance,
lastEditorData
});
onMounted(async () => {
const supports = getInstalledCKBaseFeatures();
let editorConfig = appendUsageDataPluginToConfig({ ...props.config });
let prevModelValue = model.value;
if (model.value) editorConfig = assignInitialDataToEditorConfig(editorConfig, model.value, true);
const Constructor = resolveEditorConstructor(props.editor, props.disableWatchdog, props.watchdogConfig);
try {
var _editorElementRef$val;
const domElement = (_editorElementRef$val = editorElementRef.value) === null || _editorElementRef$val === void 0 ? void 0 : _editorElementRef$val.elementRef;
if (!domElement) throw new Error("Editor element is not available. Make sure the component is mounted.");
const editor = await (supports.elementConfigAttachment ? Constructor.create(assignElementToEditorConfig(Constructor, domElement, editorConfig)) : Constructor.create(domElement, editorConfig));
if (isUnmounted.value) {
await destroyEditorWithWatchdog(editor);
return;
}
if (model.value !== prevModelValue) editor.data.set(model.value);
const watchdog = attachEditorWatchdogErrorHandler(editor, {
isUnmounted: () => isUnmounted.value,
onError: ({ error, watchdog, editor, causesRestart }) => {
if (!hasErrorHandler()) console.error(error);
emit("error", error, {
phase: "runtime",
watchdog,
editor,
causesRestart
});
}
});
if (watchdog) watchdog.on("restart", () => {
try {
if (instance.value && isClassicEditor(Constructor)) cleanupOrphanEditorElements(instance.value);
} catch (err) {
console.error(err);
}
if (!isUnmounted.value) {
instance.value = markRaw(watchdog.editor);
assignEditorDataToModel(instance.value);
}
});
instance.value = markRaw(editor);
} catch (error) {
if (isUnmounted.value) return;
if (!hasErrorHandler()) console.error(error);
emit("error", error, { phase: "initialization" });
}
});
onBeforeUnmount(async () => {
const editor = instance.value;
if (!editor) return;
instance.value = void 0;
await destroyEditorWithWatchdog(editor);
});
return (_ctx, _cache) => {
return openBlock(), createBlock(DynamicElement_default, {
ref_key: "editorElementRef",
ref: editorElementRef,
definition: unref(elementDefinition)
}, null, 8, ["definition"]);
};
}
});
//#endregion
//#region src/CkeditorElement.vue
var CkeditorElement_default = /* @__PURE__ */ defineComponent({
name: "CkeditorElement",
__name: "CkeditorElement",
props: {
editor: { default: null },
element: { default: "toolbar" }
},
setup(__props) {
const props = __props;
const uiRef = ref();
watchEffect((onCleanup) => {
var _editor$ui$view$menuB, _editor$ui$view$toolb;
const editor = props.editor;
const uiContainer = uiRef.value;
if (!editor || !uiContainer) return;
const uiElement = props.element === "menuBar" ? (_editor$ui$view$menuB = editor.ui.view.menuBarView) === null || _editor$ui$view$menuB === void 0 ? void 0 : _editor$ui$view$menuB.element : (_editor$ui$view$toolb = editor.ui.view.toolbar) === null || _editor$ui$view$toolb === void 0 ? void 0 : _editor$ui$view$toolb.element;
if (!uiElement) return;
uiContainer.appendChild(uiElement);
onCleanup(() => {
if (uiContainer.contains(uiElement)) uiContainer.removeChild(uiElement);
});
}, { flush: "post" });
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", {
ref_key: "uiRef",
ref: uiRef
}, null, 512);
};
}
});
//#endregion
//#region src/multiroot/constants.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
var ROOT_EDITABLE_OPTIONS_ATTRIBUTE = "$rootEditableOptions";
//#endregion
//#region src/multiroot/MultiRootEditorEditable.vue
var MultiRootEditorEditable_default = /* @__PURE__ */ defineComponent({
name: "CkeditorMultiRootEditable",
__name: "MultiRootEditorEditable",
props: {
id: { default: void 0 },
rootName: {},
editor: { default: null },
editableOptions: { default: null }
},
setup(__props) {
const props = __props;
const editorElementRef = ref();
const root = computed(() => {
var _props$editor$model$d, _props$editor;
return (_props$editor$model$d = (_props$editor = props.editor) === null || _props$editor === void 0 ? void 0 : _props$editor.model.document.getRoot(props.rootName)) !== null && _props$editor$model$d !== void 0 ? _props$editor$model$d : null;
});
const rootEditableOptions = computed(() => {
var _props$editableOption;
const currentRoot = root.value;
if (!currentRoot) return null;
return { ...(_props$editableOption = props.editableOptions) !== null && _props$editableOption !== void 0 ? _props$editableOption : currentRoot.getAttribute(ROOT_EDITABLE_OPTIONS_ATTRIBUTE) };
});
const elementDefinition = computed(() => {
var _options$element, _props$id;
const options = rootEditableOptions.value;
if (!options) return null;
const normalizedDefinition = normalizeEditorElementDefinition((_options$element = options.element) !== null && _options$element !== void 0 ? _options$element : { name: "div" });
return {
...normalizedDefinition,
attributes: {
...normalizedDefinition.attributes,
id: (_props$id = props.id) !== null && _props$id !== void 0 ? _props$id : props.rootName
}
};
});
watchEffect((onCleanup) => {
var _editorElementRef$val;
const editor = props.editor;
const currentRoot = root.value;
const options = rootEditableOptions.value;
const element = (_editorElementRef$val = editorElementRef.value) === null || _editorElementRef$val === void 0 ? void 0 : _editorElementRef$val.elementRef;
if (!editor || !currentRoot || !options || !element) return;
if (editor.ui.getEditableElement(props.rootName)) editor.detachEditable(currentRoot);
const editable = editor.ui.view.createEditable(props.rootName, element, options.label);
editable.isInlineRoot = !editor.model.schema.checkChild(currentRoot, "$block");
editor.ui.addEditable(editable, options.placeholder);
editor.editing.view.forceRender();
onCleanup(() => {
if (editor.state === "destroyed") return;
if (editor.model.document.getRoot(props.rootName) === currentRoot) editor.detachEditable(currentRoot);
});
}, { flush: "post" });
return (_ctx, _cache) => {
return elementDefinition.value ? (openBlock(), createBlock(DynamicElement_default, {
key: 0,
ref_key: "editorElementRef",
ref: editorElementRef,
definition: elementDefinition.value
}, null, 8, ["definition"])) : createCommentVNode("", true);
};
}
});
//#endregion
//#region src/multiroot/useMultiRootEditor.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
var INPUT_EVENT_DEBOUNCE_WAIT = 300;
var EDITOR_DESTROYED_BEFORE_READY_MESSAGE = "The editor was destroyed before it became ready.";
function useMultiRootEditor(options) {
const isUnmounted = useIsUnmounted();
const instance = ref();
const data = ref(cloneData(toValue(options.data)));
const rootsAttributes = ref(normalizeRootsAttributes(toValue(options.rootsAttributes), data.value));
const roots = ref(Object.keys(data.value));
const shouldUpdateEditor = ref(false);
const lastEditorData = ref();
const lastEditorRootsAttributes = ref();
let editorReadyCallbacks = [];
useEditorReadOnly(instance, () => toValue(options.disabled));
watch(() => toValue(options.data), (newData) => {
if (instance.value && lastEditorData.value && areRecordsEqual(newData, lastEditorData.value)) return;
setData(cloneData(newData));
}, { deep: true });
watch(() => toValue(options.rootsAttributes), (newRootsAttributes) => {
const sourceRootsAttributes = cloneRootsAttributes(newRootsAttributes);
const normalizedRootsAttributes = normalizeRootsAttributes(newRootsAttributes, data.value);
const nextRootsAttributes = instance.value ? mergeRootsAttributes(rootsAttributes.value, normalizedRootsAttributes, data.value) : normalizedRootsAttributes;
const shouldEmitNormalizedRootsAttributes = !!instance.value && !areRecordsEqual(nextRootsAttributes, sourceRootsAttributes);
if (instance.value && lastEditorRootsAttributes.value && areRecordsEqual(nextRootsAttributes, lastEditorRootsAttributes.value)) {
if (shouldEmitNormalizedRootsAttributes) {
setRootsAttributes(nextRootsAttributes);
emitRootsAttributes(null, instance.value);
}
return;
}
setRootsAttributes(nextRootsAttributes);
if (shouldEmitNormalizedRootsAttributes) emitRootsAttributes(null, instance.value);
}, { deep: true });
watch([data, rootsAttributes], () => {
const editor = instance.value;
if (!editor || !shouldUpdateEditor.value) return;
shouldUpdateEditor.value = false;
syncEditorWithState(editor);
}, {
deep: true,
flush: "post"
});
watch(instance, async (newInstance, _oldInstance, onCleanup) => {
var _options$onReady;
/* istanbul ignore if -- @preserve - Defensive check, instance can only be set to an editor here. */
if (!newInstance) return;
let isCurrentInstance = true;
const editor = newInstance;
const modelDocument = editor.model.document;
const viewDocument = editor.editing.view.document;
const emitDebouncedDataUpdate = debounce((event) => {
updateStateFromEditor(editor, event);
}, INPUT_EVENT_DEBOUNCE_WAIT, { leading: true });
const onChangeDataListener = (event) => onChangeData(editor, event, emitDebouncedDataUpdate);
const onAddRootListener = (event, root) => onAddRoot(editor, event, root);
const onDetachRootListener = (event, root) => onDetachRoot(editor, event, root);
const onFocusListener = (event) => {
var _options$onFocus;
return (_options$onFocus = options.onFocus) === null || _options$onFocus === void 0 ? void 0 : _options$onFocus.call(options, event, editor);
};
const onBlurListener = (event) => {
var _options$onBlur;
return (_options$onBlur = options.onBlur) === null || _options$onBlur === void 0 ? void 0 : _options$onBlur.call(options, event, editor);
};
const onDestroyListener = () => {
var _options$onDestroy;
emitDebouncedDataUpdate.cancel();
(_options$onDestroy = options.onDestroy) === null || _options$onDestroy === void 0 || _options$onDestroy.call(options, editor);
};
modelDocument.on("change:data", onChangeDataListener);
editor.on("addRoot", onAddRootListener);
editor.on("detachRoot", onDetachRootListener);
viewDocument.on("focus", onFocusListener);
viewDocument.on("blur", onBlurListener);
editor.once("destroy", onDestroyListener);
onCleanup(() => {
isCurrentInstance = false;
modelDocument.off("change:data", onChangeDataListener);
editor.off("addRoot", onAddRootListener);
editor.off("detachRoot", onDetachRootListener);
viewDocument.off("focus", onFocusListener);
viewDocument.off("blur", onBlurListener);
emitDebouncedDataUpdate.cancel();
});
await nextTick();
/* istanbul ignore if -- @preserve - Instance replacement/unmount before deferred ready is a defensive race guard. */
if (!isCurrentInstance || isUnmounted.value || instance.value !== editor) return;
(_options$onReady = options.onReady) === null || _options$onReady === void 0 || _options$onReady.call(options, editor);
resolveEditorReadyCallbacks(editor);
}, { flush: "post" });
onMounted(initializeEditor);
onBeforeUnmount(async () => {
const editor = instance.value;
rejectEditorReadyCallbacks(/* @__PURE__ */ new Error(EDITOR_DESTROYED_BEFORE_READY_MESSAGE));
if (!editor) return;
instance.value = void 0;
forceAssignFakeEditableElements(editor);
await destroyEditorWithWatchdog(editor);
});
async function initializeEditor() {
const creationData = cloneData(data.value);
const creationRootsAttributes = cloneRootsAttributes(rootsAttributes.value);
const Constructor = resolveEditorConstructor(toValue(options.editor), !!toValue(options.disableWatchdog), toValue(options.watchdogConfig));
try {
const editor = await createEditor(Constructor, creationData, creationRootsAttributes);
if (isUnmounted.value) {
rejectEditorReadyCallbacks(/* @__PURE__ */ new Error(EDITOR_DESTROYED_BEFORE_READY_MESSAGE));
forceAssignFakeEditableElements(editor);
await destroyEditorWithWatchdog(editor);
return;
}
const watchdog = attachEditorWatchdogErrorHandler(editor, {
isUnmounted: () => isUnmounted.value,
onError: ({ error, watchdog, editor, causesRestart }) => {
reportError(error, {
phase: "runtime",
watchdog,
editor,
causesRestart
});
}
});
if (watchdog) watchdog.on("restart", () => {
try {
/* istanbul ignore else -- @preserve - Restart is only handled after an editor instance is assigned. */
if (instance.value) cleanupOrphanEditorElements(instance.value);
} catch (err) {
console.error(err);
}
/* istanbul ignore if -- @preserve - Restart without an editor or after unmount is a watchdog edge case. */
if (isUnmounted.value || !watchdog.editor) return;
const restartedEditor = watchdog.editor;
instance.value = markRaw(restartedEditor);
syncStateFromEditor(restartedEditor);
emitData(null, restartedEditor);
emitRootsAttributes(null, restartedEditor);
});
instance.value = markRaw(editor);
if (areRecordsEqual(data.value, creationData) && areRecordsEqual(rootsAttributes.value, creationRootsAttributes)) syncStateFromEditor(editor);
else {
syncEditorWithState(editor);
syncStateFromEditor(editor);
}
} catch (error) {
rejectEditorReadyCallbacks(error);
/* istanbul ignore if -- @preserve - Initialization errors after unmount are intentionally ignored. */
if (isUnmounted.value) return;
reportError(error, { phase: "initialization" });
}
}
async function createEditor(Constructor, initialData, initialRootsAttributes) {
let editorConfig = assignAttributesPropToMultiRootEditorConfig(initialRootsAttributes, { ...toValue(options.config) });
editorConfig = appendUsageDataPluginToConfig(editorConfig);
const { initialData: mergedInitialData, ...mergedConfig } = assignInitialDataToMultirootEditorConfig(initialData, editorConfig);
const supports = getInstalledCKBaseFeatures();
const Editor = Constructor;
return await (supports.elementConfigAttachment ? Editor.create({
...mergedConfig,
initialData: mergedInitialData
}) : Editor.create(mergedInitialData, mergedConfig));
}
function syncEditorWithState(editor) {
const desiredData = cloneData(data.value);
const desiredRootsAttributes = normalizeRootsAttributes(rootsAttributes.value, desiredData);
const editorData = editor.getFullData();
const editorRootsAttributes = normalizeRootsAttributes(editor.getRootsAttributes(), editorData);
const { addedKeys: newRoots, removedKeys: removedRoots } = getRecordDiff(editorData, desiredData);
const modifiedRoots = Object.keys(desiredData).filter((rootName) => editorData[rootName] !== void 0 && editorData[rootName] !== desiredData[rootName]);
const rootsWithChangedAttributes = Object.keys(desiredRootsAttributes).filter((rootName) => editorData[rootName] !== void 0 && !areRecordsEqual(editorRootsAttributes[rootName], desiredRootsAttributes[rootName]));
rootsAttributes.value = desiredRootsAttributes;
editor.model.change((writer) => {
handleNewRoots(editor, newRoots, desiredData, desiredRootsAttributes);
handleRemovedRoots(editor, removedRoots);
if (modifiedRoots.length) updateEditorData(editor, modifiedRoots, desiredData);
if (rootsWithChangedAttributes.length) updateEditorAttributes(editor, writer, rootsWithChangedAttributes, desiredRootsAttributes);
});
}
function syncStateFromEditor(editor) {
data.value = cloneData(editor.getFullData());
rootsAttributes.value = normalizeRootsAttributes(editor.getRootsAttributes(), data.value);
roots.value = Object.keys(data.value);
}
function onChangeData(editor, event, emitDebouncedDataUpdate) {
var _options$onChange;
emitDebouncedDataUpdate(event);
(_options$onChange = options.onChange) === null || _options$onChange === void 0 || _options$onChange.call(options, event, editor);
}
function updateStateFromEditor(editor, event) {
if (toValue(options.disableTwoWayDataBinding) || isUnmounted.value) return;
const editorData = cloneData(editor.getFullData());
const editorRootsAttributes = normalizeRootsAttributes(editor.getRootsAttributes(), editorData);
const shouldEmitData = !areRecordsEqual(data.value, editorData);
const shouldEmitRootsAttributes = !areRecordsEqual(rootsAttributes.value, editorRootsAttributes);
data.value = editorData;
rootsAttributes.value = editorRootsAttributes;
roots.value = Object.keys(editorData);
lastEditorData.value = cloneData(editorData);
lastEditorRootsAttributes.value = cloneRootsAttributes(editorRootsAttributes);
if (shouldEmitData) emitData(event, editor);
if (shouldEmitRootsAttributes) emitRootsAttributes(event, editor);
}
function onAddRoot(editor, event, root) {
const rootName = root.rootName;
if (!toValue(options.disableTwoWayDataBinding)) {
data.value = {
...data.value,
[rootName]: editor.getData({ rootName })
};
rootsAttributes.value = normalizeRootsAttributes({
...rootsAttributes.value,
[rootName]: editor.getRootAttributes(rootName)
}, data.value);
emitData(event, editor);
emitRootsAttributes(event, editor);
}
roots.value = unique([...roots.value, rootName]);
}
function onDetachRoot(editor, event, root) {
const rootName = root.rootName;
if (!toValue(options.disableTwoWayDataBinding)) {
const newData = { ...data.value };
const newRootsAttributes = { ...rootsAttributes.value };
delete newData[rootName];
delete newRootsAttributes[rootName];
data.value = newData;
rootsAttributes.value = newRootsAttributes;
emitData(event, editor);
emitRootsAttributes(event, editor);
}
roots.value = roots.value.filter((currentRootName) => currentRootName !== rootName);
}
function emitData(event, editor) {
var _options$onUpdateData;
lastEditorData.value = cloneData(data.value);
(_options$onUpdateData = options.onUpdateData) === null || _options$onUpdateData === void 0 || _options$onUpdateData.call(options, cloneData(data.value), event, editor);
}
function emitRootsAttributes(event, editor) {
var _options$onUpdateRoot;
lastEditorRootsAttributes.value = cloneRootsAttributes(rootsAttributes.value);
(_options$onUpdateRoot = options.onUpdateRootsAttributes) === null || _options$onUpdateRoot === void 0 || _options$onUpdateRoot.call(options, cloneRootsAttributes(rootsAttributes.value), event, editor);
}
function reportError(error, description) {
/* istanbul ignore else -- @preserve - The Vue component always provides an error callback. */
if (options.onError) options.onError(error, description);
else console.error(error);
}
function setData(newData) {
shouldUpdateEditor.value = true;
data.value = cloneData(newData);
rootsAttributes.value = normalizeRootsAttributes(rootsAttributes.value, data.value);
if (!instance.value) roots.value = Object.keys(data.value);
}
function setRootsAttributes(newRootsAttributes) {
shouldUpdateEditor.value = true;
rootsAttributes.value = normalizeRootsAttributes(newRootsAttributes, data.value);
}
async function addRoot({ name, data: rootData = "", attributes = {}, editableOptions, ...rootOptions }) {
const editor = await waitForEditor();
const supports = getInstalledCKBaseFeatures();
editor.model.change(() => {
const mappedAttributes = {
...attributes,
...editableOptions && { ["$rootEditableOptions"]: editableOptions }
};
for (const key of Object.keys(mappedAttributes)) editor.registerRootAttribute(key);
let options = {
isUndoable: true,
...rootOptions
};
if (supports.rootsConfigEntry) options = {
...options,
initialData: rootData,
modelAttributes: mappedAttributes
};
else options = {
...options,
data: rootData,
attributes: mappedAttributes
};
editor.addRoot(name, options);
});
}
async function removeRoot(name) {
const editor = await waitForEditor();
editor.model.change(() => {
editor.detachRoot(name, true);
});
}
function waitForEditor() {
if (instance.value) return Promise.resolve(instance.value);
if (isUnmounted.value) return Promise.reject(/* @__PURE__ */ new Error(EDITOR_DESTROYED_BEFORE_READY_MESSAGE));
return new Promise((resolve, reject) => {
editorReadyCallbacks.push({
resolve,
reject
});
});
}
function resolveEditorReadyCallbacks(editor) {
editorReadyCallbacks.forEach(({ resolve }) => resolve(editor));
editorReadyCallbacks = [];
}
function rejectEditorReadyCallbacks(error) {
editorReadyCallbacks.forEach(({ reject }) => reject(error));
editorReadyCallbacks = [];
}
return {
instance,
roots,
data,
rootsAttributes,
setData,
setRootsAttributes,
addRoot,
removeRoot
};
}
function handleNewRoots(editor, rootNames, data, rootsAttributes) {
const supports = getInstalledCKBaseFeatures();
for (const rootName of rootNames) {
const rootAttributes = {
...editor.model.document.getRoot(rootName) && editor.getRootAttributes(rootName),
...rootsAttributes[rootName]
};
const rootData = data[rootName];
for (const key of Object.keys(rootAttributes)) editor.registerRootAttribute(key);
let options = { isUndoable: true };
if (supports.rootsConfigEntry) options = {
...options,
initialData: rootData,
modelAttributes: rootAttributes
};
else options = {
...options,
data: rootData,
attributes: rootAttributes
};
editor.addRoot(rootName, options);
}
}
function handleRemovedRoots(editor, rootNames) {
for (const rootName of rootNames) editor.detachRoot(rootName, true);
}
function updateEditorData(editor, rootNames, data) {
const dataToUpdate = rootNames.reduce((result, rootName) => ({
...result,
[rootName]: data[rootName]
}), Object.create(null));
editor.data.set(dataToUpdate, { suppressErrorInCollaboration: true });
}
function updateEditorAttributes(editor, writer, rootNames, rootsAttributes) {
for (const rootName of rootNames) {
const rootAttributes = rootsAttributes[rootName];
for (const key of Object.keys(rootAttributes)) editor.registerRootAttribute(key);
const root = editor.model.document.getRoot(rootName);
/* istanbul ignore if -- @preserve - Attribute updates are only requested for existing roots. */
if (!root) continue;
writer.setAttributes(rootAttributes, root);
}
}
function forceAssignFakeEditableElements(editor) {
const initializeEditableWithFakeElement = (editable) => {
if (editable.name && !editor.editing.view.getDomRoot(editable.name)) editor.editing.view.attachDomRoot(document.createElement("div"), editable.name);
};
Object.values(editor.ui.view.editables).forEach(initializeEditableWithFakeElement);
}
function normalizeRootsAttributes(rootsAttributes, data) {
/* istanbul ignore next -- @preserve - Direct composable usage may omit roots attributes. */
const source = rootsAttributes !== null && rootsAttributes !== void 0 ? rootsAttributes : {};
return Object.keys(data).reduce((result, rootName) => {
result[rootName] = { ...source[rootName] };
return result;
}, Object.create(null));
}
function mergeRootsAttributes(previousRootsAttributes, nextRootsAttributes, data) {
return Object.keys(data).reduce((result, rootName) => {
result[rootName] = {
...previousRootsAttributes[rootName],
...nextRootsAttributes[rootName]
};
return result;
}, Object.create(null));
}
function cloneData(data) {
return { ...data };
}
function cloneRootsAttributes(rootsAttributes) {
/* istanbul ignore next -- @preserve - Direct composable usage may omit roots attributes. */
return Object.keys(rootsAttributes !== null && rootsAttributes !== void 0 ? rootsAttributes : {}).reduce((result, rootName) => {
result[rootName] = { ...rootsAttributes[rootName] };
return result;
}, Object.create(null));
}
function getRecordDiff(previousState, newState) {
const previousStateKeys = Object.keys(previousState);
const newStateKeys = Object.keys(newState);
return {
addedKeys: newStateKeys.filter((key) => !previousStateKeys.includes(key)),
removedKeys: previousStateKeys.filter((key) => !newStateKeys.includes(key))
};
}
function areRecordsEqual(first, second) {
return JSON.stringify(first) === JSON.stringify(second);
}
function unique(values) {
return [...new Set(values)];
}
//#endregion
//#region src/CkeditorMultiRoot.vue
var CkeditorMultiRoot_default = /* @__PURE__ */ defineComponent({
name: "CkeditorMultiRoot",
__name: "CkeditorMultiRoot",
props: /*@__PURE__*/ mergeModels({
editor: {},
config: { default: () => ({}) },
disabled: {
type: Boolean,
default: false
},
disableTwoWayDataBinding: {
type: Boolean,
default: false
},
watchdogConfig: {},
disableWatchdog: {
type: Boolean,
default: false
}
}, {
"modelValue": { default: () => ({}) },
"modelModifiers": {},
"rootsAttributes": { default: () => ({}) },
"rootsAttributesModifiers": {}
}),
emits: /*@__PURE__*/ mergeModels([
"ready",
"destroy",
"blur",
"focus",
"input",
"update:modelValue",
"update:rootsAttributes",
"error",
"change"
], ["update:modelValue", "update:rootsAttributes"]),
setup(__props, { expose: __expose, emit: __emit }) {
const model = useModel(__props, "modelValue");
const rootsAttributesModel = useModel(__props, "rootsAttributes");
const props = __props;
const emit = __emit;
const currentInstance = getCurrentInstance();
const hasErrorHandler = () => {
var _currentInstance$vnod;
return !!(currentInstance === null || currentInstance === void 0 || (_currentInstance$vnod = currentInstance.vnode.props) === null || _currentInstance$vnod === void 0 ? void 0 : _currentInstance$vnod.onError);
};
const { instance, roots, data, rootsAttributes, addRoot, removeRoot } = useMultiRootEditor({
editor: () => props.editor,
config: () => props.config,
data: model,
rootsAttributes: rootsAttributesModel,
disabled: () => props.disabled,
disableWatchdog: () => props.disableWatchdog,
disableTwoWayDataBinding: () => props.disableTwoWayDataBinding,
watchdogConfig: () => props.watchdogConfig,
onReady: (editor) => emit("ready", editor),
onDestroy: (editor) => emit("destroy", editor),
onFocus: (event, editor) => emit("focus", event, editor),
onBlur: (event, editor) => emit("blur", event, editor),
onChange: (event, editor) => emit("change", event, editor),
onUpdateData: (data, event, editor) => {
emit("update:modelValue", data, event, editor);
emit("input", data, event, editor);
},
onUpdateRootsAttributes: (rootsAttributes, event, editor) => {
emit("update:rootsAttributes", rootsAttributes, event, editor);
},
onError: (error, description) => {
if (!hasErrorHandler()) console.error(error);
emit("error", error, description);
}
});
__expose({
instance,
roots,
data,
rootsAttributes,
addRoot,
removeRoot
});
return (_ctx, _cache) => {
return renderSlot(_ctx.$slots, "default", {
editor: unref(instance),
roots: unref(roots),
data: unref(data),
attributes: unref(rootsAttributes),
rootsAttributes: unref(rootsAttributes),
addRoot: unref(addRoot),
removeRoot: unref(removeRoot)
}, () => [
createVNode(CkeditorElement_default, {
editor: unref(instance),
element: "menuBar"
}, null, 8, ["editor"]),
createVNode(CkeditorElement_default, { editor: unref(instance) }, null, 8, ["editor"]),
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(roots), (rootName) => {
return openBlock(), createBlock(MultiRootEditorEditable_default, {
id: rootName,
key: rootName,
"root-name": rootName,
editor: unref(instance)
}, null, 8, [
"id",
"root-name",
"editor"
]);
}), 128))
]);
};
}
});
//#endregion
//#region src/composables/useAsync.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* A composable that executes an async function and provides the result.
*
* @param asyncFunc The async function to execute.
* @returns The result of the async function.
* @example
*
* ```ts
* const { loading, data, error } = useAsync( async () => {
* const response = await fetch( 'https://api.example.com/data' );
* return response.json();
* } );
* ```
*/
var useAsync = (asyncFunc) => {
const lastQueryUUID = ref(null);
const error = ref(null);
const data = ref(null);
const loading = computed(() => lastQueryUUID.value !== null);
watchEffect(async () => {
const currentQueryUID = uid();
lastQueryUUID.value = currentQueryUID;
data.value = null;
error.value = null;
const shouldDiscardQuery = () => lastQueryUUID.value !== currentQueryUID;
try {
const result = await asyncFunc();
if (!shouldDiscardQuery()) data.value = result;
} catch (err) {
console.error(err);
if (!shouldDiscardQuery()) error.value = err;
} finally {
if (!shouldDiscardQuery()) lastQueryUUID.value = null;
}
});
return {
loading: shallowReadonly(loading),
data: shallowReadonly(data),
error: shallowReadonly(error)
};
};
//#endregion
//#region src/useCKEditorCloud.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* A composable function that loads CKEditor Cloud services.
*
* @param config The configuration of the CKEditor Cloud services.
* @returns The result of the loaded CKEditor Cloud services.
* @template Config The type of the CKEditor Cloud configuration.
* @example
* ```ts
* const { data } = useCKEditorCloud( {
* version: '43.0.0',
* languages: [ 'en', 'de' ],
* premium: true
* } );
*
* if ( data.value ) {
* const { CKEditor, CKEditorPremiumFeatures } = data.value;
* const { Paragraph } = CKEditor;
*
* // ..
* }
*/
function useCKEditorCloud(config) {
return useAsync(() => loadCKEditorCloud$1(toValue(config)));
}
//#endregion
//#region src/plugin.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/* istanbul ignore if -- @preserve */
if (!Vue.version || !Vue.version.startsWith("3.")) throw new Error("The CKEditor plugin works only with Vue 3+. For more information, please refer to https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/vuejs-v3.html");
var CkeditorPlugin = {
/**
* Installs the plugin, registering the `<ckeditor>` component.
*
* @param app The application instance.
*/
install(app) {
app.component("Ckeditor", Ckeditor_default);
app.component("CkeditorElement", CkeditorElement_default);
app.component("CkeditorMultiRoot", CkeditorMultiRoot_default);
app.component("CkeditorMultiRootEditable", MultiRootEditorEditable_default);
} };
//#endregion
export { Ckeditor_default as Ckeditor, CkeditorElement_default as CkeditorElement, CkeditorMultiRoot_default as CkeditorMultiRoot, MultiRootEditorEditable_default as CkeditorMultiRootEditable, CkeditorPlugin, loadCKEditorCloud, useCKEditorCloud, useMultiRootEditor };
//# sourceMappingURL=ckeditor.js.map