@ckeditor/ckeditor5-vue
Version:
Official Vue.js 3+ component for CKEditor 5 – the best browser-based rich text editor.
440 lines (439 loc) • 16 kB
JavaScript
import * as Vue from "vue";
import { computed, createBlock, defineComponent, getCurrentInstance, markRaw, mergeModels, onBeforeUnmount, onMounted, openBlock, ref, resolveDynamicComponent, shallowReadonly, toValue, useModel, version, watch, watchEffect } from "vue";
import { appendExtraPluginsToEditorConfig, assignElementToEditorConfig, assignInitialDataToEditorConfig, 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.0.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
/**
* 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 = ref(false);
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) {
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 = 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, { 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/ckeditor.vue
var ckeditor_default = /* @__PURE__ */ defineComponent({
name: "CKEditor",
__name: "ckeditor",
props: /* @__PURE__ */ 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__ */ 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 element = ref();
const instance = 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
});
onMounted(async () => {
const supports = getInstalledCKBaseFeatures();
let editorConfig = appendUsageDataPluginToConfig({ ...props.config });
let prevModelValue = model.value;
if (model.value) editorConfig = 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(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 = 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(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 = 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);
} };
//#endregion
export { ckeditor_default as Ckeditor, CkeditorPlugin, loadCKEditorCloud, useCKEditorCloud };
//# sourceMappingURL=ckeditor.js.map