class CKEditorContextComponent extends HTMLElement { static get observedAttributes() { return ['plugins', 'config']; } /** @type {import('ckeditor5').Context|null} */ instance = null; /** @type {Promise} */ instancePromise = Promise.withResolvers(); /** @type {Set} */ #connectedEditors = new Set(); /** @type {String} Attributes checksum hash */ #integrity = ''; async connectedCallback() { this.#integrity = this.getAttribute('integrity'); try { execIfDOMReady(() => this.#initializeContext()); } catch (error) { console.error('Failed to initialize context:', error); this.dispatchEvent(new CustomEvent('context-error', { detail: error })); } } async attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== null && oldValue !== newValue) { await this.#initializeContext(); } } async disconnectedCallback() { if (this.instance) { await this.instance.destroy(); this.instance = null; } } /** * Register editor component with this context * * @param {CKEditorComponent} editor */ registerEditor(editor) { this.#connectedEditors.add(editor); } /** * Unregister editor component from this context * * @param {CKEditorComponent} editor */ unregisterEditor(editor) { this.#connectedEditors.delete(editor); } /** * Validates editor configuration integrity hash to prevent attacks. */ async #validateIntegrity() { const integrity = await calculateChecksum({ plugins: this.getAttribute('plugins'), }); if (integrity !== this.#integrity) { throw new Error( 'Configuration integrity check failed. It means that #integrity attributes mismatch from attributes passed to webcomponent. ' + 'This could be a security issue. Please check if you are passing correct attributes to the webcomponent.' ); } } /** * Initialize CKEditor context with shared configuration * * @private */ async #initializeContext() { if (this.instance) { this.instancePromise = Promise.withResolvers(); await this.instance.destroy(); this.instance = null; } await this.#validateIntegrity(); const { Context, ContextWatchdog } = await import('ckeditor5'); const plugins = await this.#getPlugins(); const config = this.#getConfig(); this.instance = new ContextWatchdog(Context, { crashNumberLimit: 10 }); await this.instance.create({ ...config, plugins }); this.instance.on('itemError', (...args) => { console.error('Context item error:', ...args); }); this.instancePromise.resolve(this.instance); this.dispatchEvent(new CustomEvent('context-ready', { detail: this.instance })); // Reinitialize connected editors. await Promise.all( [...this.#connectedEditors].map(editor => editor.reinitializeEditor()) ); } async #getPlugins() { const raw = this.getAttribute('plugins'); return loadAsyncImports(raw ? JSON.parse(raw) : []); } /** * Gets context configuration with resolved element references. * * @private */ #getConfig() { const config = JSON.parse(this.getAttribute('config') || '{}'); return resolveElementReferences(config); } } customElements.define('ckeditor-context-component', CKEditorContextComponent);