@ckeditor/ckeditor5-editor-multi-root
Advanced tools
Comparing version 37.0.0-alpha.3 to 37.0.0-rc.0
/*! | ||
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/(()=>{var t={704:(t,e,o)=>{t.exports=o(79)("./src/core.js")},492:(t,e,o)=>{t.exports=o(79)("./src/engine.js")},273:(t,e,o)=>{t.exports=o(79)("./src/ui.js")},209:(t,e,o)=>{t.exports=o(79)("./src/utils.js")},434:(t,e,o)=>{t.exports=o(79)("./src/watchdog.js")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function o(i){var r=e[i];if(void 0!==r)return r.exports;var n=e[i]={exports:{}};return t[i](n,n.exports,o),n.exports}o.d=(t,e)=>{for(var i in e)o.o(e,i)&&!o.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),o.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var i={};(()=>{"use strict";o.r(i),o.d(i,{MultiRootEditor:()=>W});var t=o(704),e=o(209),r=o(434),n=o(273),s=o(492);class c extends n.EditorUI{constructor(t,e){super(t),this.view=e}init(){const t=this.view,e=this.editor.editing.view;let o;for(const e of Object.keys(t.editables))t.editables[e].name=e;t.render(),this.focusTracker.on("change:focusedElement",((t,e,i)=>{for(const t of Object.values(this.view.editables))i===t.element&&(o=t.element)})),this.focusTracker.on("change:isFocused",((t,e,i)=>{i||(o=null)}));for(const t of Object.values(this.view.editables)){const i=t.element;this.setEditableElement(t.name,i),t.bind("isFocused").to(this.focusTracker,"isFocused",this.focusTracker,"focusedElement",((t,e)=>!!t&&(e===i||o===i))),e.attachDomRoot(i,t.name)}this._initPlaceholder(),this._initToolbar(),this.fire("ready")}destroy(){super.destroy();const t=this.view,e=this.editor.editing.view;for(const t of Object.values(this.view.editables))e.detachDomRoot(t.name);t.destroy()}_initToolbar(){const t=this.editor,e=this.view;e.toolbar.fillFromConfig(t.config.get("toolbar"),this.componentFactory),this.addToolbar(e.toolbar)}_initPlaceholder(){const t=this.editor,e=t.editing.view,o=t.config.get("placeholder");if(o)for(const t of Object.values(this.view.editables)){const i=e.document.getRoot(t.name),r="string"==typeof o?o:o[t.name];r&&(0,s.enablePlaceholder)({view:e,element:i,text:r,isDirectHost:!1,keepOnFocus:!0})}}}class l extends n.EditorUIView{constructor(t,e,o,i={}){super(t);const r=t.t;this.toolbar=new n.ToolbarView(t,{shouldGroupWhenFull:i.shouldToolbarGroupWhenFull}),this.editables={};for(const s of o){const o=new n.InlineEditableUIView(t,e,i.editableElements?i.editableElements[s]:void 0,{label:t=>r("Rich Text Editor. Editing area: %0",t.name)});this.editables[s]=o}this.editable=Object.values(this.editables)[0],this.toolbar.extendTemplate({attributes:{class:["ck-reset_all","ck-rounded-corners"],dir:t.uiLanguageDirection}})}render(){super.render(),this.registerChild(Object.values(this.editables)),this.registerChild([this.toolbar])}}const a=function(t){return null!=t&&"object"==typeof t};const d="object"==typeof global&&global&&global.Object===Object&&global;var u="object"==typeof self&&self&&self.Object===Object&&self;const h=(d||u||Function("return this")()).Symbol;var f=Object.prototype,b=f.hasOwnProperty,g=f.toString,p=h?h.toStringTag:void 0;const v=function(t){var e=b.call(t,p),o=t[p];try{t[p]=void 0;var i=!0}catch(t){}var r=g.call(t);return i&&(e?t[p]=o:delete t[p]),r};var m=Object.prototype.toString;const y=function(t){return m.call(t)};var j="[object Null]",w="[object Undefined]",E=h?h.toStringTag:void 0;const O=function(t){return null==t?void 0===t?w:j:E&&E in Object(t)?v(t):y(t)};const x=function(t,e){return function(o){return t(e(o))}}(Object.getPrototypeOf,Object);var T="[object Object]",D=Function.prototype,F=Object.prototype,S=D.toString,C=F.hasOwnProperty,P=S.call(Object);const k=function(t){if(!a(t)||O(t)!=T)return!1;var e=x(t);if(null===e)return!0;var o=C.call(e,"constructor")&&e.constructor;return"function"==typeof o&&o instanceof o&&S.call(o)==P};const R=function(t){return a(t)&&1===t.nodeType&&!k(t)};class W extends((0,t.DataApiMixin)(t.Editor)){constructor(o,i={}){const r=Object.keys(o),n=0===r.length||"string"==typeof o[r[0]];if(n&&void 0!==i.initialData)throw new e.CKEditorError("editor-create-initial-data",null);if(super(i),n||(this.sourceElements=o),void 0===this.config.get("initialData")){const t={};for(const i of r)t[i]=_(s=o[i])?(0,e.getDataFromElement)(s):s;this.config.set("initialData",t)}var s;if(!n)for(const e of r)(0,t.secureSourceElement)(this,o[e]);for(const t of r)this.model.document.createRoot("$root",t);const a={shouldToolbarGroupWhenFull:!this.config.get("toolbar.shouldNotGroupWhenFull"),editableElements:n?void 0:o},d=new l(this.locale,this.editing.view,r,a);this.ui=new c(this,d)}destroy(){const t=this.config.get("updateSourceElementOnDestroy"),o={};if(this.sourceElements)for(const e of Object.keys(this.sourceElements))o[e]=t?this.getData({rootName:e}):"";return this.ui.destroy(),super.destroy().then((()=>{if(this.sourceElements)for(const t of Object.keys(this.sourceElements))(0,e.setDataInElement)(this.sourceElements[t],o[t])}))}static create(t,o={}){return new Promise((i=>{for(const o of Object.values(t))if(_(o)&&"TEXTAREA"===o.tagName)throw new e.CKEditorError("editor-wrong-element",null);const r=new this(t,o);i(r.initPlugins().then((()=>r.ui.init())).then((()=>r.data.init(r.config.get("initialData")))).then((()=>r.fire("ready"))).then((()=>r)))}))}}function _(t){return R(t)}W.Context=t.Context,W.EditorWatchdog=r.EditorWatchdog,W.ContextWatchdog=r.ContextWatchdog})(),(window.CKEditor5=window.CKEditor5||{}).editorMultiRoot=i})(); | ||
*/(()=>{var t={704:(t,e,o)=>{t.exports=o(79)("./src/core.js")},492:(t,e,o)=>{t.exports=o(79)("./src/engine.js")},273:(t,e,o)=>{t.exports=o(79)("./src/ui.js")},209:(t,e,o)=>{t.exports=o(79)("./src/utils.js")},434:(t,e,o)=>{t.exports=o(79)("./src/watchdog.js")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function o(i){var s=e[i];if(void 0!==s)return s.exports;var r=e[i]={exports:{}};return t[i](r,r.exports,o),r.exports}o.d=(t,e)=>{for(var i in e)o.o(e,i)&&!o.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),o.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var i={};(()=>{"use strict";o.r(i),o.d(i,{MultiRootEditor:()=>A});var t=o(704),e=o(209),s=o(434),r=o(273),n=o(492);class a extends r.EditorUI{constructor(t,e){super(t),this.view=e,this._lastFocusedEditableElement=null}init(){this.view.render(),this.focusTracker.on("change:focusedElement",((t,e,o)=>{for(const t of Object.values(this.view.editables))o===t.element&&(this._lastFocusedEditableElement=t.element)})),this.focusTracker.on("change:isFocused",((t,e,o)=>{o||(this._lastFocusedEditableElement=null)}));for(const t of Object.values(this.view.editables))this.addEditable(t);this._initToolbar(),this.fire("ready")}addEditable(t,e){const o=t.element;this.editor.editing.view.attachDomRoot(o,t.name),this.setEditableElement(t.name,o),t.bind("isFocused").to(this.focusTracker,"isFocused",this.focusTracker,"focusedElement",((t,e)=>!!t&&(e===o||this._lastFocusedEditableElement===o))),this._initPlaceholder(t,e)}removeEditable(t){this.editor.editing.view.detachDomRoot(t.name),t.unbind("isFocused"),this.removeEditableElement(t.name)}destroy(){super.destroy();for(const t of Object.values(this.view.editables))this.removeEditable(t);this.view.destroy()}_initToolbar(){const t=this.editor,e=this.view;e.toolbar.fillFromConfig(t.config.get("toolbar"),this.componentFactory),this.addToolbar(e.toolbar)}_initPlaceholder(t,e){if(!e){const o=this.editor.config.get("placeholder");o&&(e="string"==typeof o?o:o[t.name])}if(!e)return;const o=this.editor.editing.view,i=o.document.getRoot(t.name);(0,n.enablePlaceholder)({view:o,element:i,text:e,isDirectHost:!1,keepOnFocus:!0})}}class c extends r.EditorUIView{constructor(t,e,o,i={}){super(t),this._editingView=e,this.toolbar=new r.ToolbarView(t,{shouldGroupWhenFull:i.shouldToolbarGroupWhenFull}),this.editables={};for(const t of o){const e=i.editableElements?i.editableElements[t]:void 0;this.createEditable(t,e)}this.editable=Object.values(this.editables)[0],this.toolbar.extendTemplate({attributes:{class:["ck-reset_all","ck-rounded-corners"],dir:t.uiLanguageDirection}})}createEditable(t,e){const o=this.locale.t,i=new r.InlineEditableUIView(this.locale,this._editingView,e,{label:t=>o("Rich Text Editor. Editing area: %0",t.name)});return this.editables[t]=i,i.name=t,this.isRendered&&this.registerChild(i),i}removeEditable(t){const e=this.editables[t];this.isRendered&&this.deregisterChild(e),delete this.editables[t],e.destroy()}render(){super.render(),this.registerChild(Object.values(this.editables)),this.registerChild(this.toolbar)}}const l=function(t){return null!=t&&"object"==typeof t};const d="object"==typeof global&&global&&global.Object===Object&&global;var u="object"==typeof self&&self&&self.Object===Object&&self;const h=(d||u||Function("return this")()).Symbol;var b=Object.prototype,f=b.hasOwnProperty,m=b.toString,g=h?h.toStringTag:void 0;const E=function(t){var e=f.call(t,g),o=t[g];try{t[g]=void 0;var i=!0}catch(t){}var s=m.call(t);return i&&(e?t[g]=o:delete t[g]),s};var v=Object.prototype.toString;const p=function(t){return v.call(t)};var y=h?h.toStringTag:void 0;const w=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":y&&y in Object(t)?E(t):p(t)};const j=function(t,e){return function(o){return t(e(o))}}(Object.getPrototypeOf,Object);var O=Function.prototype,R=Object.prototype,x=O.toString,C=R.hasOwnProperty,F=x.call(Object);const T=function(t){if(!l(t)||"[object Object]"!=w(t))return!1;var e=j(t);if(null===e)return!0;var o=C.call(e,"constructor")&&e.constructor;return"function"==typeof o&&o instanceof o&&x.call(o)==F};const _=function(t){return l(t)&&1===t.nodeType&&!T(t)};class A extends((0,t.DataApiMixin)(t.Editor)){constructor(o,i={}){const s=Object.keys(o),r=0===s.length||"string"==typeof o[s[0]];if(r&&void 0!==i.initialData)throw new e.CKEditorError("editor-create-initial-data",null);if(super(i),this._registeredRootsAttributesKeys=new Set,this.sourceElements=r?{}:o,void 0===this.config.get("initialData")){const t={};for(const i of s)t[i]=D(n=o[i])?(0,e.getDataFromElement)(n):n;this.config.set("initialData",t)}var n;if(!r)for(const e of s)(0,t.secureSourceElement)(this,o[e]);for(const t of s)this.model.document.createRoot("$root",t);if(this.config.get("rootsAttributes")){const t=this.config.get("rootsAttributes");for(const[o,i]of Object.entries(t)){if(!s.includes(o))throw new e.CKEditorError("multi-root-editor-root-attributes-no-root",null);for(const t of Object.keys(i))this._registeredRootsAttributesKeys.add(t)}this.data.on("init",(()=>{this.model.enqueueChange({isUndoable:!1},(e=>{for(const[o,i]of Object.entries(t)){const t=this.model.document.getRoot(o);for(const[o,s]of Object.entries(i))null!==s&&e.setAttribute(o,s,t)}}))}))}const l={shouldToolbarGroupWhenFull:!this.config.get("toolbar.shouldNotGroupWhenFull"),editableElements:r?void 0:o},d=new c(this.locale,this.editing.view,s,l);this.ui=new a(this,d),this.model.document.on("change:data",(()=>{const t=this.model.document.differ.getChangedRoots();for(const e of t){const t=this.model.document.getRoot(e.name);"attached"==e.state?this.fire("addRoot",t):"detached"==e.state&&this.fire("detachRoot",t)}}))}destroy(){const t=this.config.get("updateSourceElementOnDestroy"),o={};if(this.sourceElements)for(const e of Object.keys(this.sourceElements))o[e]=t?this.getData({rootName:e}):"";return this.ui.destroy(),super.destroy().then((()=>{if(this.sourceElements)for(const t of Object.keys(this.sourceElements))(0,e.setDataInElement)(this.sourceElements[t],o[t])}))}addRoot(t,{data:e="",attributes:o={},elementName:i="$root",isUndoable:s=!1}={}){const r=this.data,n=this._registeredRootsAttributesKeys;function a(s){const a=s.addRoot(t,i);e&&s.insert(r.parse(e,a),a,0);for(const t of Object.keys(o))n.add(t),s.setAttribute(t,o[t],a)}s?this.model.change(a):this.model.enqueueChange({isUndoable:!1},a)}detachRoot(t,e=!1){e?this.model.change((e=>e.detachRoot(t))):this.model.enqueueChange({isUndoable:!1},(e=>e.detachRoot(t)))}createEditable(t,e){const o=this.ui.view.createEditable(t.rootName);return this.ui.addEditable(o,e),this.editing.view.forceRender(),o.element}detachEditable(t){const e=t.rootName,o=this.ui.view.editables[e];return this.ui.removeEditable(o),this.ui.view.removeEditable(e),o.element}getFullData(t){const e={};for(const o of this.model.document.getRootNames())e[o]=this.data.get({...t,rootName:o});return e}getRootsAttributes(){const t={},e=Array.from(this._registeredRootsAttributesKeys);for(const o of this.model.document.getRootNames()){t[o]={};const i=this.model.document.getRoot(o);for(const s of e)t[o][s]=i.hasAttribute(s)?i.getAttribute(s):null}return t}static create(t,o={}){return new Promise((i=>{for(const o of Object.values(t))if(D(o)&&"TEXTAREA"===o.tagName)throw new e.CKEditorError("editor-wrong-element",null);const s=new this(t,o);i(s.initPlugins().then((()=>s.ui.init())).then((()=>s.data.init(s.config.get("initialData")))).then((()=>s.fire("ready"))).then((()=>s)))}))}}function D(t){return _(t)}A.Context=t.Context,A.EditorWatchdog=s.EditorWatchdog,A.ContextWatchdog=s.ContextWatchdog})(),(window.CKEditor5=window.CKEditor5||{}).editorMultiRoot=i})(); |
{ | ||
"name": "@ckeditor/ckeditor5-editor-multi-root", | ||
"version": "37.0.0-alpha.3", | ||
"version": "37.0.0-rc.0", | ||
"description": "Multi-root editor implementation for CKEditor 5.", | ||
@@ -14,19 +14,19 @@ "keywords": [ | ||
"dependencies": { | ||
"ckeditor5": "^37.0.0-alpha.3", | ||
"ckeditor5": "^37.0.0-rc.0", | ||
"lodash-es": "^4.17.15" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-core": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-dev-utils": "^35.0.0", | ||
"@ckeditor/ckeditor5-engine": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-enter": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-heading": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-paragraph": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-theme-lark": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-typing": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-ui": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-undo": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-utils": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-watchdog": "^37.0.0-alpha.3", | ||
"@ckeditor/ckeditor5-basic-styles": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-core": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-dev-utils": "^36.0.0", | ||
"@ckeditor/ckeditor5-engine": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-enter": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-heading": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-paragraph": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-typing": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-ui": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-undo": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-utils": "^37.0.0-rc.0", | ||
"@ckeditor/ckeditor5-watchdog": "^37.0.0-rc.0", | ||
"typescript": "^4.8.4", | ||
@@ -37,3 +37,3 @@ "webpack": "^5.58.1", | ||
"engines": { | ||
"node": ">=14.0.0", | ||
"node": ">=16.0.0", | ||
"npm": ">=5.7.1" | ||
@@ -40,0 +40,0 @@ }, |
@@ -9,1 +9,2 @@ /** | ||
export { default as MultiRootEditor } from './multirooteditor'; | ||
import './augmentation'; |
@@ -9,1 +9,2 @@ /** | ||
export { default as MultiRootEditor } from './multirooteditor'; | ||
import './augmentation'; |
@@ -11,2 +11,3 @@ /** | ||
import MultiRootEditorUI from './multirooteditorui'; | ||
import { type RootElement } from 'ckeditor5/src/engine'; | ||
declare const MultiRootEditor_base: import("ckeditor5/src/utils").Mixed<typeof Editor, import("ckeditor5/src/core").DataApi>; | ||
@@ -51,4 +52,9 @@ /** | ||
*/ | ||
readonly sourceElements: Record<string, HTMLElement> | undefined; | ||
readonly sourceElements: Record<string, HTMLElement>; | ||
/** | ||
* Holds attributes keys that were passed in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} | ||
* config property and should be returned by {@link #getRootsAttributes}. | ||
*/ | ||
private readonly _registeredRootsAttributesKeys; | ||
/** | ||
* Creates an instance of the multi-root editor. | ||
@@ -91,2 +97,166 @@ * | ||
/** | ||
* Adds a new root to the editor. | ||
* | ||
* ```ts | ||
* editor.addRoot( 'myRoot', { data: '<p>Initial root data.</p>' } ); | ||
* ``` | ||
* | ||
* After a root is added, you will be able to modify and retrieve its data. | ||
* | ||
* All root names must be unique. An error will be thrown if you will try to create a root with the name same as | ||
* an already existing, attached root. However, you can call this method for a detached root. See also {@link #detachRoot}. | ||
* | ||
* Whenever a root is added, the editor instance will fire {@link #event:addRoot `addRoot` event}. The event is also called when | ||
* the root is added indirectly, e.g. by the undo feature or on a remote client during real-time collaboration. | ||
* | ||
* Note, that this method only adds a root to the editor model. It **does not** create a DOM editable element for the new root. | ||
* Until such element is created (and attached to the root), the root is "virtual": it is not displayed anywhere and its data can | ||
* be changed only using the editor API. | ||
* | ||
* To create a DOM editable element for the root, listen to {@link #event:addRoot `addRoot` event} and call {@link #createEditable}. | ||
* Then, insert the DOM element in a desired place, that will depend on the integration with your application and your requirements. | ||
* | ||
* ```ts | ||
* editor.on( 'addRoot', ( evt, root ) => { | ||
* const editableElement = editor.createEditable( root ); | ||
* | ||
* // You may want to create a more complex DOM structure here. | ||
* // | ||
* // Alternatively, you may want to create a DOM structure before | ||
* // calling `editor.addRoot()` and only append `editableElement` at | ||
* // a proper place. | ||
* | ||
* document.querySelector( '#editors' ).appendChild( editableElement ); | ||
* } ); | ||
* | ||
* // ... | ||
* | ||
* editor.addRoot( 'myRoot' ); // Will create a root, a DOM editable element and append it to `#editors` container element. | ||
* ``` | ||
* | ||
* You can set root attributes on the new root while you add it: | ||
* | ||
* ```ts | ||
* // Add a collapsed root at fourth position from top. | ||
* // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes. | ||
* editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } ); | ||
* ``` | ||
* | ||
* See also {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes` configuration option}. | ||
* | ||
* Note that attributes keys of attributes added in `attributes` option are also included in {@link #getRootsAttributes} return value. | ||
* | ||
* By setting `isUndoable` flag to `true`, you can allow for detaching the root using the undo feature. | ||
* | ||
* Additionally, you can group adding multiple roots in one undo step. This can be useful if you add multiple roots that are | ||
* combined into one, bigger UI element, and want them all to be undone together. | ||
* | ||
* ```ts | ||
* let rowId = 0; | ||
* | ||
* editor.model.change( () => { | ||
* editor.addRoot( 'left-row-' + rowId, { isUndoable: true } ); | ||
* editor.addRoot( 'center-row-' + rowId, { isUndoable: true } ); | ||
* editor.addRoot( 'right-row-' + rowId, { isUndoable: true } ); | ||
* | ||
* rowId++; | ||
* } ); | ||
* ``` | ||
* | ||
* @param rootName Name of the root to add. | ||
* @param options Additional options for the added root. | ||
*/ | ||
addRoot(rootName: string, { data, attributes, elementName, isUndoable }?: AddRootOptions): void; | ||
/** | ||
* Detaches a root from the editor. | ||
* | ||
* ```ts | ||
* editor.detachRoot( 'myRoot' ); | ||
* ``` | ||
* | ||
* A detached root is not entirely removed from the editor model, however it can be considered removed. | ||
* | ||
* After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it, | ||
* it is automatically removed as well. Finally, a detached root is not returned by | ||
* {@link module:engine/model/document~Document#getRootNames} by default. | ||
* | ||
* It is possible to re-add a previously detached root calling {@link #addRoot}. | ||
* | ||
* Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also | ||
* called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration. | ||
* | ||
* Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with | ||
* the root and it **does not** remove the DOM element from the DOM structure of your application. | ||
* | ||
* To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event} | ||
* and call {@link #detachEditable}. Then, remove the DOM element from your application. | ||
* | ||
* ```ts | ||
* editor.on( 'detachRoot', ( evt, root ) => { | ||
* const editableElement = editor.detachEditable( root ); | ||
* | ||
* // You may want to do an additional DOM clean-up here. | ||
* | ||
* editableElement.remove(); | ||
* } ); | ||
* | ||
* // ... | ||
* | ||
* editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element. | ||
* ``` | ||
* | ||
* By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature. | ||
* | ||
* Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one, | ||
* bigger UI element, and you want them all to be re-added together. | ||
* | ||
* ```ts | ||
* editor.model.change( () => { | ||
* editor.detachRoot( 'left-row-3', true ); | ||
* editor.detachRoot( 'center-row-3', true ); | ||
* editor.detachRoot( 'right-row-3', true ); | ||
* } ); | ||
* ``` | ||
* | ||
* @param rootName Name of the root to detach. | ||
* @param isUndoable Whether detaching the root can be undone (using the undo feature) or not. | ||
*/ | ||
detachRoot(rootName: string, isUndoable?: boolean): void; | ||
/** | ||
* Creates and returns a new DOM editable element for the given root element. | ||
* | ||
* The new DOM editable is attached to the model root and can be used to modify the root content. | ||
* | ||
* @param root Root for which the editable element should be created. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
* @returns The created DOM element. Append it in a desired place in your application. | ||
*/ | ||
createEditable(root: RootElement, placeholder?: string): HTMLElement; | ||
/** | ||
* Detaches the DOM editable element that was attached to the given root. | ||
* | ||
* @param root Root for which the editable element should be detached. | ||
* @returns The DOM element that was detached. You may want to remove it from your application DOM structure. | ||
*/ | ||
detachEditable(root: RootElement): HTMLElement; | ||
/** | ||
* Returns the document data for all attached roots. | ||
* | ||
* @param options Additional configuration for the retrieved data. | ||
* Editor features may introduce more configuration options that can be set through this parameter. | ||
* @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default, | ||
* which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming | ||
* use `'none'`. In such cases exact content will be returned (for example `'<p> </p>'` for an empty editor). | ||
* @returns The full document data. | ||
*/ | ||
getFullData(options?: Record<string, unknown>): Record<string, string>; | ||
/** | ||
* Returns currently set roots attributes for attributes specified in | ||
* {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} configuration option. | ||
* | ||
* @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root. | ||
*/ | ||
getRootsAttributes(): Record<string, RootAttributes>; | ||
/** | ||
* Creates a new multi-root editor instance. | ||
@@ -245,2 +415,59 @@ * | ||
} | ||
/** | ||
* Fired whenever a root is {@link ~MultiRootEditor#addRoot added or re-added} to the editor model. | ||
* | ||
* Use this event to {@link ~MultiRootEditor#createEditable create a DOM editable} for the added root and append the DOM element | ||
* in a desired place in your application. | ||
* | ||
* The event is fired after all changes from a given batch are applied. The event is not fired, if the root was added and detached | ||
* in the same batch. | ||
* | ||
* @eventName ~MultiRootEditor#addRoot | ||
* @param root The root that was added. | ||
*/ | ||
export type AddRootEvent = { | ||
name: 'addRoot'; | ||
args: [root: RootElement]; | ||
}; | ||
/** | ||
* Fired whenever a root is {@link ~MultiRootEditor#detachRoot detached} from the editor model. | ||
* | ||
* Use this event to {@link ~MultiRootEditor#detachEditable destroy a DOM editable} for the detached root and remove the DOM element | ||
* from your application. | ||
* | ||
* The event is fired after all changes from a given batch are applied. The event is not fired, if the root was detached and re-added | ||
* in the same batch. | ||
* | ||
* @eventName ~MultiRootEditor#detachRoot | ||
* @param root The root that was detached. | ||
*/ | ||
export type DetachRootEvent = { | ||
name: 'detachRoot'; | ||
args: [root: RootElement]; | ||
}; | ||
/** | ||
* Additional options available when adding a root. | ||
*/ | ||
export type AddRootOptions = { | ||
/** | ||
* Initial data for the root. | ||
*/ | ||
data?: string; | ||
/** | ||
* Initial attributes for the root. | ||
*/ | ||
attributes?: RootAttributes; | ||
/** | ||
* Element name for the root element in the model. It can be used to set different schema rules for different roots. | ||
*/ | ||
elementName?: string; | ||
/** | ||
* Whether creating the root can be undone (using the undo feature) or not. | ||
*/ | ||
isUndoable?: boolean; | ||
}; | ||
/** | ||
* Attributes set on a model root element. | ||
*/ | ||
export type RootAttributes = Record<string, unknown>; | ||
export {}; |
@@ -65,5 +65,13 @@ /** | ||
super(config); | ||
/** | ||
* Holds attributes keys that were passed in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} | ||
* config property and should be returned by {@link #getRootsAttributes}. | ||
*/ | ||
this._registeredRootsAttributesKeys = new Set(); | ||
if (!sourceIsData) { | ||
this.sourceElements = sourceElementsOrData; | ||
} | ||
else { | ||
this.sourceElements = {}; | ||
} | ||
if (this.config.get('initialData') === undefined) { | ||
@@ -86,2 +94,33 @@ // Create initial data object containing data from all roots. | ||
} | ||
if (this.config.get('rootsAttributes')) { | ||
const rootsAttributes = this.config.get('rootsAttributes'); | ||
for (const [rootName, attributes] of Object.entries(rootsAttributes)) { | ||
if (!rootNames.includes(rootName)) { | ||
/** | ||
* Trying to set attributes on a non-existing root. | ||
* | ||
* Roots specified in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes} do not match initial | ||
* editor roots. | ||
* | ||
* @error multi-root-editor-root-attributes-no-root | ||
*/ | ||
throw new CKEditorError('multi-root-editor-root-attributes-no-root', null); | ||
} | ||
for (const key of Object.keys(attributes)) { | ||
this._registeredRootsAttributesKeys.add(key); | ||
} | ||
} | ||
this.data.on('init', () => { | ||
this.model.enqueueChange({ isUndoable: false }, writer => { | ||
for (const [name, attributes] of Object.entries(rootsAttributes)) { | ||
const root = this.model.document.getRoot(name); | ||
for (const [key, value] of Object.entries(attributes)) { | ||
if (value !== null) { | ||
writer.setAttribute(key, value, root); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
const options = { | ||
@@ -93,2 +132,14 @@ shouldToolbarGroupWhenFull: !this.config.get('toolbar.shouldNotGroupWhenFull'), | ||
this.ui = new MultiRootEditorUI(this, view); | ||
this.model.document.on('change:data', () => { | ||
const changedRoots = this.model.document.differ.getChangedRoots(); | ||
for (const changes of changedRoots) { | ||
const root = this.model.document.getRoot(changes.name); | ||
if (changes.state == 'attached') { | ||
this.fire('addRoot', root); | ||
} | ||
else if (changes.state == 'detached') { | ||
this.fire('detachRoot', root); | ||
} | ||
} | ||
}); | ||
} | ||
@@ -141,2 +192,220 @@ /** | ||
/** | ||
* Adds a new root to the editor. | ||
* | ||
* ```ts | ||
* editor.addRoot( 'myRoot', { data: '<p>Initial root data.</p>' } ); | ||
* ``` | ||
* | ||
* After a root is added, you will be able to modify and retrieve its data. | ||
* | ||
* All root names must be unique. An error will be thrown if you will try to create a root with the name same as | ||
* an already existing, attached root. However, you can call this method for a detached root. See also {@link #detachRoot}. | ||
* | ||
* Whenever a root is added, the editor instance will fire {@link #event:addRoot `addRoot` event}. The event is also called when | ||
* the root is added indirectly, e.g. by the undo feature or on a remote client during real-time collaboration. | ||
* | ||
* Note, that this method only adds a root to the editor model. It **does not** create a DOM editable element for the new root. | ||
* Until such element is created (and attached to the root), the root is "virtual": it is not displayed anywhere and its data can | ||
* be changed only using the editor API. | ||
* | ||
* To create a DOM editable element for the root, listen to {@link #event:addRoot `addRoot` event} and call {@link #createEditable}. | ||
* Then, insert the DOM element in a desired place, that will depend on the integration with your application and your requirements. | ||
* | ||
* ```ts | ||
* editor.on( 'addRoot', ( evt, root ) => { | ||
* const editableElement = editor.createEditable( root ); | ||
* | ||
* // You may want to create a more complex DOM structure here. | ||
* // | ||
* // Alternatively, you may want to create a DOM structure before | ||
* // calling `editor.addRoot()` and only append `editableElement` at | ||
* // a proper place. | ||
* | ||
* document.querySelector( '#editors' ).appendChild( editableElement ); | ||
* } ); | ||
* | ||
* // ... | ||
* | ||
* editor.addRoot( 'myRoot' ); // Will create a root, a DOM editable element and append it to `#editors` container element. | ||
* ``` | ||
* | ||
* You can set root attributes on the new root while you add it: | ||
* | ||
* ```ts | ||
* // Add a collapsed root at fourth position from top. | ||
* // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes. | ||
* editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } ); | ||
* ``` | ||
* | ||
* See also {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes` configuration option}. | ||
* | ||
* Note that attributes keys of attributes added in `attributes` option are also included in {@link #getRootsAttributes} return value. | ||
* | ||
* By setting `isUndoable` flag to `true`, you can allow for detaching the root using the undo feature. | ||
* | ||
* Additionally, you can group adding multiple roots in one undo step. This can be useful if you add multiple roots that are | ||
* combined into one, bigger UI element, and want them all to be undone together. | ||
* | ||
* ```ts | ||
* let rowId = 0; | ||
* | ||
* editor.model.change( () => { | ||
* editor.addRoot( 'left-row-' + rowId, { isUndoable: true } ); | ||
* editor.addRoot( 'center-row-' + rowId, { isUndoable: true } ); | ||
* editor.addRoot( 'right-row-' + rowId, { isUndoable: true } ); | ||
* | ||
* rowId++; | ||
* } ); | ||
* ``` | ||
* | ||
* @param rootName Name of the root to add. | ||
* @param options Additional options for the added root. | ||
*/ | ||
addRoot(rootName, { data = '', attributes = {}, elementName = '$root', isUndoable = false } = {}) { | ||
const dataController = this.data; | ||
const registeredKeys = this._registeredRootsAttributesKeys; | ||
if (isUndoable) { | ||
this.model.change(_addRoot); | ||
} | ||
else { | ||
this.model.enqueueChange({ isUndoable: false }, _addRoot); | ||
} | ||
function _addRoot(writer) { | ||
const root = writer.addRoot(rootName, elementName); | ||
if (data) { | ||
writer.insert(dataController.parse(data, root), root, 0); | ||
} | ||
for (const key of Object.keys(attributes)) { | ||
registeredKeys.add(key); | ||
writer.setAttribute(key, attributes[key], root); | ||
} | ||
} | ||
} | ||
/** | ||
* Detaches a root from the editor. | ||
* | ||
* ```ts | ||
* editor.detachRoot( 'myRoot' ); | ||
* ``` | ||
* | ||
* A detached root is not entirely removed from the editor model, however it can be considered removed. | ||
* | ||
* After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it, | ||
* it is automatically removed as well. Finally, a detached root is not returned by | ||
* {@link module:engine/model/document~Document#getRootNames} by default. | ||
* | ||
* It is possible to re-add a previously detached root calling {@link #addRoot}. | ||
* | ||
* Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also | ||
* called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration. | ||
* | ||
* Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with | ||
* the root and it **does not** remove the DOM element from the DOM structure of your application. | ||
* | ||
* To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event} | ||
* and call {@link #detachEditable}. Then, remove the DOM element from your application. | ||
* | ||
* ```ts | ||
* editor.on( 'detachRoot', ( evt, root ) => { | ||
* const editableElement = editor.detachEditable( root ); | ||
* | ||
* // You may want to do an additional DOM clean-up here. | ||
* | ||
* editableElement.remove(); | ||
* } ); | ||
* | ||
* // ... | ||
* | ||
* editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element. | ||
* ``` | ||
* | ||
* By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature. | ||
* | ||
* Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one, | ||
* bigger UI element, and you want them all to be re-added together. | ||
* | ||
* ```ts | ||
* editor.model.change( () => { | ||
* editor.detachRoot( 'left-row-3', true ); | ||
* editor.detachRoot( 'center-row-3', true ); | ||
* editor.detachRoot( 'right-row-3', true ); | ||
* } ); | ||
* ``` | ||
* | ||
* @param rootName Name of the root to detach. | ||
* @param isUndoable Whether detaching the root can be undone (using the undo feature) or not. | ||
*/ | ||
detachRoot(rootName, isUndoable = false) { | ||
if (isUndoable) { | ||
this.model.change(writer => writer.detachRoot(rootName)); | ||
} | ||
else { | ||
this.model.enqueueChange({ isUndoable: false }, writer => writer.detachRoot(rootName)); | ||
} | ||
} | ||
/** | ||
* Creates and returns a new DOM editable element for the given root element. | ||
* | ||
* The new DOM editable is attached to the model root and can be used to modify the root content. | ||
* | ||
* @param root Root for which the editable element should be created. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
* @returns The created DOM element. Append it in a desired place in your application. | ||
*/ | ||
createEditable(root, placeholder) { | ||
const editable = this.ui.view.createEditable(root.rootName); | ||
this.ui.addEditable(editable, placeholder); | ||
this.editing.view.forceRender(); | ||
return editable.element; | ||
} | ||
/** | ||
* Detaches the DOM editable element that was attached to the given root. | ||
* | ||
* @param root Root for which the editable element should be detached. | ||
* @returns The DOM element that was detached. You may want to remove it from your application DOM structure. | ||
*/ | ||
detachEditable(root) { | ||
const rootName = root.rootName; | ||
const editable = this.ui.view.editables[rootName]; | ||
this.ui.removeEditable(editable); | ||
this.ui.view.removeEditable(rootName); | ||
return editable.element; | ||
} | ||
/** | ||
* Returns the document data for all attached roots. | ||
* | ||
* @param options Additional configuration for the retrieved data. | ||
* Editor features may introduce more configuration options that can be set through this parameter. | ||
* @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default, | ||
* which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming | ||
* use `'none'`. In such cases exact content will be returned (for example `'<p> </p>'` for an empty editor). | ||
* @returns The full document data. | ||
*/ | ||
getFullData(options) { | ||
const data = {}; | ||
for (const rootName of this.model.document.getRootNames()) { | ||
data[rootName] = this.data.get({ ...options, rootName }); | ||
} | ||
return data; | ||
} | ||
/** | ||
* Returns currently set roots attributes for attributes specified in | ||
* {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} configuration option. | ||
* | ||
* @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root. | ||
*/ | ||
getRootsAttributes() { | ||
const rootsAttributes = {}; | ||
const keys = Array.from(this._registeredRootsAttributesKeys); | ||
for (const rootName of this.model.document.getRootNames()) { | ||
rootsAttributes[rootName] = {}; | ||
const root = this.model.document.getRoot(rootName); | ||
for (const key of keys) { | ||
rootsAttributes[rootName][key] = root.hasAttribute(key) ? root.getAttribute(key) : null; | ||
} | ||
} | ||
return rootsAttributes; | ||
} | ||
/** | ||
* Creates a new multi-root editor instance. | ||
@@ -143,0 +412,0 @@ * |
@@ -9,3 +9,3 @@ /** | ||
import { type Editor } from 'ckeditor5/src/core'; | ||
import { EditorUI } from 'ckeditor5/src/ui'; | ||
import { EditorUI, type InlineEditableUIView } from 'ckeditor5/src/ui'; | ||
import type MultiRootEditorUIView from './multirooteditoruiview'; | ||
@@ -21,2 +21,6 @@ /** | ||
/** | ||
* The editable element that was focused the last time when any of the editables had focus. | ||
*/ | ||
private _lastFocusedEditableElement; | ||
/** | ||
* Creates an instance of the multi-root editor UI class. | ||
@@ -33,2 +37,26 @@ * | ||
/** | ||
* Adds the editable to the editor UI. | ||
* | ||
* After the editable is added to the editor UI it can be considered "active". | ||
* | ||
* The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and | ||
* changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized. | ||
* | ||
* @param editable The editable instance to add. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
*/ | ||
addEditable(editable: InlineEditableUIView, placeholder?: string): void; | ||
/** | ||
* Removes the editable instance from the editor UI. | ||
* | ||
* Removed editable can be considered "deactivated". | ||
* | ||
* The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in | ||
* {@link #addEditable} is removed. | ||
* | ||
* @param editable Editable to remove from the editor UI. | ||
*/ | ||
removeEditable(editable: InlineEditableUIView): void; | ||
/** | ||
* @inheritDoc | ||
@@ -42,5 +70,9 @@ */ | ||
/** | ||
* Enable the placeholder text on the editing roots, if any was configured. | ||
* Enables the placeholder text on a given editable, if the placeholder was configured. | ||
* | ||
* @param editable Editable on which the placeholder should be set. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
*/ | ||
private _initPlaceholder; | ||
} |
@@ -20,2 +20,3 @@ /** | ||
this.view = view; | ||
this._lastFocusedEditableElement = null; | ||
} | ||
@@ -27,8 +28,2 @@ /** | ||
const view = this.view; | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
let lastFocusedEditableElement; | ||
for (const editableName of Object.keys(view.editables)) { | ||
view.editables[editableName].name = editableName; | ||
} | ||
view.render(); | ||
@@ -43,3 +38,3 @@ // Keep track of the last focused editable element. Knowing which one was focused | ||
if (focusedElement === editable.element) { | ||
lastFocusedEditableElement = editable.element; | ||
this._lastFocusedEditableElement = editable.element; | ||
} | ||
@@ -54,44 +49,8 @@ } | ||
if (!isFocused) { | ||
lastFocusedEditableElement = null; | ||
this._lastFocusedEditableElement = null; | ||
} | ||
}); | ||
for (const editable of Object.values(this.view.editables)) { | ||
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered. | ||
// But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`. | ||
const editableElement = editable.element; | ||
// Register each editable UI view in the editor. | ||
this.setEditableElement(editable.name, editableElement); | ||
// Let the editable UI element respond to the changes in the global editor focus | ||
// tracker. It has been added to the same tracker a few lines above but, in reality, there are | ||
// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long | ||
// as they have focus, the editable should act like it is focused too (although technically | ||
// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user. | ||
// Doing otherwise will result in editable focus styles disappearing, once e.g. the | ||
// toolbar gets focused. | ||
editable.bind('isFocused').to(this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement', (isFocused, focusedElement) => { | ||
// When the focus tracker is blurred, it means the focus moved out of the editor UI. | ||
// No editable will maintain focus then. | ||
if (!isFocused) { | ||
return false; | ||
} | ||
// If the focus tracker says the editor UI is focused and currently focused element | ||
// is the editable, then the editable should be visually marked as focused too. | ||
if (focusedElement === editableElement) { | ||
return true; | ||
} | ||
// If the focus tracker says the editor UI is focused but the focused element is | ||
// not an editable, it is possible that the editable is still (context–)focused. | ||
// For instance, the focused element could be an input inside of a balloon attached | ||
// to the content in the editable. In such case, the editable should remain _visually_ | ||
// focused even though technically the focus is somewhere else. The focus moved from | ||
// the editable to the input but the focus context remained the same. | ||
else { | ||
return lastFocusedEditableElement === editableElement; | ||
} | ||
}); | ||
// Bind the editable UI element to the editing view, making it an end– and entry–point | ||
// of the editor's engine. This is where the engine meets the UI. | ||
editingView.attachDomRoot(editableElement, editable.name); | ||
this.addEditable(editable); | ||
} | ||
this._initPlaceholder(); | ||
this._initToolbar(); | ||
@@ -101,2 +60,68 @@ this.fire('ready'); | ||
/** | ||
* Adds the editable to the editor UI. | ||
* | ||
* After the editable is added to the editor UI it can be considered "active". | ||
* | ||
* The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and | ||
* changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized. | ||
* | ||
* @param editable The editable instance to add. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
*/ | ||
addEditable(editable, placeholder) { | ||
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered. | ||
// But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`. | ||
const editableElement = editable.element; | ||
// Bind the editable UI element to the editing view, making it an end– and entry–point | ||
// of the editor's engine. This is where the engine meets the UI. | ||
this.editor.editing.view.attachDomRoot(editableElement, editable.name); | ||
// Register each editable UI view in the editor. | ||
this.setEditableElement(editable.name, editableElement); | ||
// Let the editable UI element respond to the changes in the global editor focus | ||
// tracker. It has been added to the same tracker a few lines above but, in reality, there are | ||
// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long | ||
// as they have focus, the editable should act like it is focused too (although technically | ||
// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user. | ||
// Doing otherwise will result in editable focus styles disappearing, once e.g. the | ||
// toolbar gets focused. | ||
editable.bind('isFocused').to(this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement', (isFocused, focusedElement) => { | ||
// When the focus tracker is blurred, it means the focus moved out of the editor UI. | ||
// No editable will maintain focus then. | ||
if (!isFocused) { | ||
return false; | ||
} | ||
// If the focus tracker says the editor UI is focused and currently focused element | ||
// is the editable, then the editable should be visually marked as focused too. | ||
if (focusedElement === editableElement) { | ||
return true; | ||
} | ||
// If the focus tracker says the editor UI is focused but the focused element is | ||
// not an editable, it is possible that the editable is still (context–)focused. | ||
// For instance, the focused element could be an input inside of a balloon attached | ||
// to the content in the editable. In such case, the editable should remain _visually_ | ||
// focused even though technically the focus is somewhere else. The focus moved from | ||
// the editable to the input but the focus context remained the same. | ||
else { | ||
return this._lastFocusedEditableElement === editableElement; | ||
} | ||
}); | ||
this._initPlaceholder(editable, placeholder); | ||
} | ||
/** | ||
* Removes the editable instance from the editor UI. | ||
* | ||
* Removed editable can be considered "deactivated". | ||
* | ||
* The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in | ||
* {@link #addEditable} is removed. | ||
* | ||
* @param editable Editable to remove from the editor UI. | ||
*/ | ||
removeEditable(editable) { | ||
this.editor.editing.view.detachDomRoot(editable.name); | ||
editable.unbind('isFocused'); | ||
this.removeEditableElement(editable.name); | ||
} | ||
/** | ||
* @inheritDoc | ||
@@ -106,8 +131,6 @@ */ | ||
super.destroy(); | ||
const view = this.view; | ||
const editingView = this.editor.editing.view; | ||
for (const editable of Object.values(this.view.editables)) { | ||
editingView.detachDomRoot(editable.name); | ||
this.removeEditable(editable); | ||
} | ||
view.destroy(); | ||
this.view.destroy(); | ||
} | ||
@@ -122,29 +145,32 @@ /** | ||
toolbar.fillFromConfig(editor.config.get('toolbar'), this.componentFactory); | ||
// Register the toolbar so it becomes available for Alt+F10 and Esc navigation. | ||
// Register the toolbar, so it becomes available for Alt+F10 and Esc navigation. | ||
this.addToolbar(view.toolbar); | ||
} | ||
/** | ||
* Enable the placeholder text on the editing roots, if any was configured. | ||
* Enables the placeholder text on a given editable, if the placeholder was configured. | ||
* | ||
* @param editable Editable on which the placeholder should be set. | ||
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the | ||
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). | ||
*/ | ||
_initPlaceholder() { | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
const placeholder = editor.config.get('placeholder'); | ||
_initPlaceholder(editable, placeholder) { | ||
if (!placeholder) { | ||
const configPlaceholder = this.editor.config.get('placeholder'); | ||
if (configPlaceholder) { | ||
placeholder = typeof configPlaceholder === 'string' ? configPlaceholder : configPlaceholder[editable.name]; | ||
} | ||
} | ||
if (!placeholder) { | ||
return; | ||
} | ||
for (const editable of Object.values(this.view.editables)) { | ||
const editingRoot = editingView.document.getRoot(editable.name); | ||
const placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[editable.name]; | ||
if (placeholderText) { | ||
enablePlaceholder({ | ||
view: editingView, | ||
element: editingRoot, | ||
text: placeholderText, | ||
isDirectHost: false, | ||
keepOnFocus: true | ||
}); | ||
} | ||
} | ||
const editingView = this.editor.editing.view; | ||
const editingRoot = editingView.document.getRoot(editable.name); | ||
enablePlaceholder({ | ||
view: editingView, | ||
element: editingRoot, | ||
text: placeholder, | ||
isDirectHost: false, | ||
keepOnFocus: true | ||
}); | ||
} | ||
} |
@@ -31,2 +31,6 @@ /** | ||
/** | ||
* The editing view instance this view is related to. | ||
*/ | ||
private readonly _editingView; | ||
/** | ||
* Creates an instance of the multi-root editor UI view. | ||
@@ -51,2 +55,19 @@ * | ||
/** | ||
* Creates an editable instance with given name and registers it in the editor UI view. | ||
* | ||
* If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new | ||
* DOM element and use it instead. | ||
* | ||
* @param editableName The name for the editable. | ||
* @param editableElement DOM element for which the editable should be created. | ||
* @returns The created editable instance. | ||
*/ | ||
createEditable(editableName: string, editableElement?: HTMLElement): InlineEditableUIView; | ||
/** | ||
* Destroys and removes the editable from the editor UI view. | ||
* | ||
* @param editableName The name of the editable that should be removed. | ||
*/ | ||
removeEditable(editableName: string): void; | ||
/** | ||
* @inheritDoc | ||
@@ -53,0 +74,0 @@ */ |
@@ -36,3 +36,3 @@ /** | ||
super(locale); | ||
const t = locale.t; | ||
this._editingView = editingView; | ||
this.toolbar = new ToolbarView(locale, { | ||
@@ -44,8 +44,4 @@ shouldGroupWhenFull: options.shouldToolbarGroupWhenFull | ||
for (const editableName of editableNames) { | ||
const editable = new InlineEditableUIView(locale, editingView, options.editableElements ? options.editableElements[editableName] : undefined, { | ||
label: editable => { | ||
return t('Rich Text Editor. Editing area: %0', editable.name); | ||
} | ||
}); | ||
this.editables[editableName] = editable; | ||
const editableElement = options.editableElements ? options.editableElements[editableName] : undefined; | ||
this.createEditable(editableName, editableElement); | ||
} | ||
@@ -68,2 +64,39 @@ this.editable = Object.values(this.editables)[0]; | ||
/** | ||
* Creates an editable instance with given name and registers it in the editor UI view. | ||
* | ||
* If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new | ||
* DOM element and use it instead. | ||
* | ||
* @param editableName The name for the editable. | ||
* @param editableElement DOM element for which the editable should be created. | ||
* @returns The created editable instance. | ||
*/ | ||
createEditable(editableName, editableElement) { | ||
const t = this.locale.t; | ||
const editable = new InlineEditableUIView(this.locale, this._editingView, editableElement, { | ||
label: editable => { | ||
return t('Rich Text Editor. Editing area: %0', editable.name); | ||
} | ||
}); | ||
this.editables[editableName] = editable; | ||
editable.name = editableName; | ||
if (this.isRendered) { | ||
this.registerChild(editable); | ||
} | ||
return editable; | ||
} | ||
/** | ||
* Destroys and removes the editable from the editor UI view. | ||
* | ||
* @param editableName The name of the editable that should be removed. | ||
*/ | ||
removeEditable(editableName) { | ||
const editable = this.editables[editableName]; | ||
if (this.isRendered) { | ||
this.deregisterChild(editable); | ||
} | ||
delete this.editables[editableName]; | ||
editable.destroy(); | ||
} | ||
/** | ||
* @inheritDoc | ||
@@ -74,4 +107,4 @@ */ | ||
this.registerChild(Object.values(this.editables)); | ||
this.registerChild([this.toolbar]); | ||
this.registerChild(this.toolbar); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
87475
15
1579
Updatedckeditor5@^37.0.0-rc.0