UNPKG

@ckeditor/ckeditor5-vue

Version:

Official Vue.js 3+ component for CKEditor 5 – the best browser-based rich text editor.

477 lines (476 loc) 18.5 kB
(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("vue"), require("@ckeditor/ckeditor5-integrations-common"), require("lodash-es")) : typeof define === "function" && define.amd ? define([ "exports", "vue", "@ckeditor/ckeditor5-integrations-common", "lodash-es" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.CKEDITOR_VUE = {}, global.Vue, global.CKEDITOR_INTEGRATIONS_COMMON, global._)); })(this, function(exports, vue, _ckeditor_ckeditor5_integrations_common, lodash_es) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion vue = __toESM(vue, 1); //#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 = (0, _ckeditor_ckeditor5_integrations_common.createIntegrationUsageDataPlugin)("vue", { version: "8.0.0", frameworkVersion: vue.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 ((0, _ckeditor_ckeditor5_integrations_common.isCKEditorFreeLicense)(editorConfig.licenseKey)) return editorConfig; return (0, _ckeditor_ckeditor5_integrations_common.appendExtraPluginsToEditorConfig)(editorConfig, [VueIntegrationUsageDataPlugin]); } //#endregion //#region src/utils/cleanupOrphanEditorElements.ts /** * 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 var EDITOR_WATCHDOG_SYMBOL = Symbol.for("vue-editor-watchdog"); /** * `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, 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; } /** * 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 = (0, vue.ref)(false); (0, vue.onBeforeUnmount)(() => { isUnmounted.value = true; }); return isUnmounted; } //#endregion //#region src/composables/useEditorLifecycleEvents.ts /** * Hook that watches editor lifecycle events and maps them to Vue event emitters. */ function useEditorLifecycleEvents(instance, emit) { (0, vue.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 var INPUT_EVENT_DEBOUNCE_WAIT = 300; /** * Hook that synchronizes editor state with currently set vue model. */ function useEditorVModel({ disableTwoWayDataBinding, emit, instance, model }) { const lastEditorData = (0, vue.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); } (0, vue.watch)(model, (newModel) => { if (instance.value && newModel !== lastEditorData.value) instance.value.data.set(newModel); }); (0, vue.watch)(instance, (newInstance, _oldInstance, onCleanup) => { /* istanbul ignore if -- @preserve - Defensive check, instance never becomes undefined. */ if (!newInstance) return; const emitDebouncedInputEvent = (0, lodash_es.debounce)((evt) => { if ((0, vue.toValue)(disableTwoWayDataBinding) || isUnmounted.value) return; assignEditorDataToModel(newInstance, evt); }, INPUT_EVENT_DEBOUNCE_WAIT, { 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) { (0, vue.watchEffect)(() => { const editor = (0, vue.toValue)(instance); const isDisabled = !!(0, vue.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 ((0, _ckeditor_ckeditor5_integrations_common.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/ckeditor.vue var ckeditor_default = /* @__PURE__ */ (0, vue.defineComponent)({ name: "CKEditor", __name: "ckeditor", props: /* @__PURE__ */ (0, vue.mergeModels)({ editor: {}, config: { default: () => ({}) }, tagName: { default: "div" }, disabled: { type: Boolean, default: false }, disableTwoWayDataBinding: { type: Boolean, default: false }, watchdogConfig: {}, disableWatchdog: { type: Boolean, default: false } }, { "modelValue": { type: String, default: "" }, "modelModifiers": {} }), emits: /* @__PURE__ */ (0, vue.mergeModels)([ "ready", "destroy", "blur", "focus", "input", "update:modelValue", "error" ], ["update:modelValue"]), setup(__props, { expose: __expose, emit: __emit }) { const model = (0, vue.useModel)(__props, "modelValue"); const props = __props; const emit = __emit; const currentInstance = (0, vue.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 element = (0, vue.ref)(); const instance = (0, vue.ref)(); const isUnmounted = useIsUnmounted(); const { lastEditorData, assignEditorDataToModel } = useEditorVModel({ disableTwoWayDataBinding: () => props.disableTwoWayDataBinding, model, emit, instance }); useEditorVersionCheck(); useEditorLifecycleEvents(instance, emit); useEditorReadOnly(instance, () => props.disabled); __expose({ instance, lastEditorData }); (0, vue.onMounted)(async () => { const supports = (0, _ckeditor_ckeditor5_integrations_common.getInstalledCKBaseFeatures)(); let editorConfig = appendUsageDataPluginToConfig({ ...props.config }); let prevModelValue = model.value; if (model.value) editorConfig = (0, _ckeditor_ckeditor5_integrations_common.assignInitialDataToEditorConfig)(editorConfig, model.value, true); let Constructor = props.editor; if (!props.disableWatchdog) Constructor = wrapWithWatchdogIfPresent(props.editor, props.watchdogConfig); try { const editor = await (supports.elementConfigAttachment ? Constructor.create((0, _ckeditor_ckeditor5_integrations_common.assignElementToEditorConfig)(Constructor, element.value, editorConfig)) : Constructor.create(element.value, editorConfig)); if (isUnmounted.value) { await destroyEditorWithWatchdog(editor); return; } if (model.value !== prevModelValue) editor.data.set(model.value); const watchdog = unwrapEditorWatchdog(editor); if (watchdog) { watchdog.on("error", (_, { error, causesRestart }) => { if (isUnmounted.value) return; if (!hasErrorHandler()) console.error(error); emit("error", error, { phase: "runtime", watchdog, editor: watchdog.editor, causesRestart }); }); watchdog.on("restart", () => { try { if (instance.value) cleanupOrphanEditorElements(instance.value); } catch (err) { console.error(err); } if (!isUnmounted.value) { instance.value = (0, vue.markRaw)(watchdog.editor); assignEditorDataToModel(instance.value); } }); } instance.value = (0, vue.markRaw)(editor); } catch (error) { if (isUnmounted.value) return; if (!hasErrorHandler()) console.error(error); emit("error", error, { phase: "initialization" }); } }); (0, vue.onBeforeUnmount)(async () => { const editor = instance.value; if (!editor) return; instance.value = void 0; await destroyEditorWithWatchdog(editor); }); return (_ctx, _cache) => { return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)(__props.tagName), { ref_key: "element", ref: element }, null, 512); }; } }); //#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 = (0, vue.ref)(null); const error = (0, vue.ref)(null); const data = (0, vue.ref)(null); const loading = (0, vue.computed)(() => lastQueryUUID.value !== null); (0, vue.watchEffect)(async () => { const currentQueryUID = (0, _ckeditor_ckeditor5_integrations_common.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: (0, vue.shallowReadonly)(loading), data: (0, vue.shallowReadonly)(data), error: (0, vue.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(() => (0, _ckeditor_ckeditor5_integrations_common.loadCKEditorCloud)((0, vue.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); } }; //#endregion exports.Ckeditor = ckeditor_default; exports.CkeditorPlugin = CkeditorPlugin; Object.defineProperty(exports, "loadCKEditorCloud", { enumerable: true, get: function() { return _ckeditor_ckeditor5_integrations_common.loadCKEditorCloud; } }); exports.useCKEditorCloud = useCKEditorCloud; }); //# sourceMappingURL=ckeditor.umd.cjs.map