@ckeditor/ckeditor5-editor-multi-root 37.0.0-alpha.3 → 37.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  /*!
2
2
  * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md.
4
- */(()=>{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})();
4
+ */(()=>{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})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-editor-multi-root",
3
- "version": "37.0.0-alpha.3",
3
+ "version": "37.0.0-rc.0",
4
4
  "description": "Multi-root editor implementation for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -11,29 +11,29 @@
11
11
  ],
12
12
  "main": "src/index.js",
13
13
  "dependencies": {
14
- "ckeditor5": "^37.0.0-alpha.3",
14
+ "ckeditor5": "^37.0.0-rc.0",
15
15
  "lodash-es": "^4.17.15"
16
16
  },
17
17
  "devDependencies": {
18
- "@ckeditor/ckeditor5-basic-styles": "^37.0.0-alpha.3",
19
- "@ckeditor/ckeditor5-core": "^37.0.0-alpha.3",
20
- "@ckeditor/ckeditor5-dev-utils": "^35.0.0",
21
- "@ckeditor/ckeditor5-engine": "^37.0.0-alpha.3",
22
- "@ckeditor/ckeditor5-enter": "^37.0.0-alpha.3",
23
- "@ckeditor/ckeditor5-heading": "^37.0.0-alpha.3",
24
- "@ckeditor/ckeditor5-paragraph": "^37.0.0-alpha.3",
25
- "@ckeditor/ckeditor5-theme-lark": "^37.0.0-alpha.3",
26
- "@ckeditor/ckeditor5-typing": "^37.0.0-alpha.3",
27
- "@ckeditor/ckeditor5-ui": "^37.0.0-alpha.3",
28
- "@ckeditor/ckeditor5-undo": "^37.0.0-alpha.3",
29
- "@ckeditor/ckeditor5-utils": "^37.0.0-alpha.3",
30
- "@ckeditor/ckeditor5-watchdog": "^37.0.0-alpha.3",
18
+ "@ckeditor/ckeditor5-basic-styles": "^37.0.0-rc.0",
19
+ "@ckeditor/ckeditor5-core": "^37.0.0-rc.0",
20
+ "@ckeditor/ckeditor5-dev-utils": "^36.0.0",
21
+ "@ckeditor/ckeditor5-engine": "^37.0.0-rc.0",
22
+ "@ckeditor/ckeditor5-enter": "^37.0.0-rc.0",
23
+ "@ckeditor/ckeditor5-heading": "^37.0.0-rc.0",
24
+ "@ckeditor/ckeditor5-paragraph": "^37.0.0-rc.0",
25
+ "@ckeditor/ckeditor5-theme-lark": "^37.0.0-rc.0",
26
+ "@ckeditor/ckeditor5-typing": "^37.0.0-rc.0",
27
+ "@ckeditor/ckeditor5-ui": "^37.0.0-rc.0",
28
+ "@ckeditor/ckeditor5-undo": "^37.0.0-rc.0",
29
+ "@ckeditor/ckeditor5-utils": "^37.0.0-rc.0",
30
+ "@ckeditor/ckeditor5-watchdog": "^37.0.0-rc.0",
31
31
  "typescript": "^4.8.4",
32
32
  "webpack": "^5.58.1",
33
33
  "webpack-cli": "^4.9.0"
34
34
  },
35
35
  "engines": {
36
- "node": ">=14.0.0",
36
+ "node": ">=16.0.0",
37
37
  "npm": ">=5.7.1"
38
38
  },
39
39
  "author": "CKSource (http://cksource.com/)",
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { type RootAttributes } from './multirooteditor';
6
+ declare module '@ckeditor/ckeditor5-core' {
7
+ interface EditorConfig {
8
+ /**
9
+ * Initial roots attributes for the document roots.
10
+ *
11
+ * **Note: This configuration option is supported only by the
12
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor multi-root} editor type.**
13
+ *
14
+ * **Note: You must provide full set of attributes for each root. If an attribute is not set on a root, set the value to `null`.
15
+ * Only provided attribute keys will be returned by
16
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#getRootsAttributes}.**
17
+ *
18
+ * Roots attributes hold additional data related to the document roots, in addition to the regular document data (which usually is
19
+ * HTML). In roots attributes, for each root, you can store arbitrary key-value pairs with attributes connected with that root.
20
+ * Use it to store any custom data that is specific to your integration or custom features.
21
+ *
22
+ * Currently, roots attributes are not used only by any official plugins. This is a mechanism that is prepared for custom features
23
+ * and non-standard integrations. If you do not provide any custom feature that would use root attributes, you do not need to
24
+ * handle (save and load) this property.
25
+ *
26
+ * ```ts
27
+ * MultiRootEditor.create(
28
+ * // Roots for the editor:
29
+ * {
30
+ * uid1: document.querySelector( '#uid1' ),
31
+ * uid2: document.querySelector( '#uid2' ),
32
+ * uid3: document.querySelector( '#uid3' ),
33
+ * uid4: document.querySelector( '#uid4' )
34
+ * },
35
+ * // Config:
36
+ * {
37
+ * rootsAttributes: {
38
+ * uid1: { order: 20, isLocked: false }, // Third, unlocked.
39
+ * uid2: { order: 10, isLocked: true }, // Second, locked.
40
+ * uid3: { order: 30, isLocked: true }, // Fourth, locked.
41
+ * uid4: { order: 0, isLocked: false } // First, unlocked.
42
+ * }
43
+ * }
44
+ * )
45
+ * .then( ... )
46
+ * .catch( ... );
47
+ * ```
48
+ *
49
+ * Note, that the above code snippet is only an example. You need to implement your own features that will use these attributes.
50
+ *
51
+ * Roots attributes can be changed the same way as attributes set on other model nodes:
52
+ *
53
+ * ```ts
54
+ * editor.model.change( writer => {
55
+ * const root = editor.model.getRoot( 'uid3' );
56
+ *
57
+ * writer.setAttribute( 'order', 40, root );
58
+ * } );
59
+ * ```
60
+ *
61
+ * You can react to root attributes changes by listening to
62
+ * {@link module:engine/model/document~Document#event:change:data document `change:data` event}:
63
+ *
64
+ * ```ts
65
+ * editor.model.document.on( 'change:data', () => {
66
+ * const changedRoots = editor.model.document.differ.getChangedRoots();
67
+ *
68
+ * for ( const change of changedRoots ) {
69
+ * if ( change.attributes ) {
70
+ * const root = editor.model.getRoot( change.name );
71
+ *
72
+ * // ...
73
+ * }
74
+ * }
75
+ * } );
76
+ * ```
77
+ */
78
+ rootsAttributes?: Record<string, RootAttributes>;
79
+ }
80
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ export {};
package/src/index.d.ts CHANGED
@@ -6,3 +6,4 @@
6
6
  * @module editor-multi-root
7
7
  */
8
8
  export { default as MultiRootEditor } from './multirooteditor';
9
+ import './augmentation';
package/src/index.js CHANGED
@@ -6,3 +6,4 @@
6
6
  * @module editor-multi-root
7
7
  */
8
8
  export { default as MultiRootEditor } from './multirooteditor';
9
+ import './augmentation';
@@ -8,6 +8,7 @@
8
8
  import { Editor, Context, type EditorConfig } from 'ckeditor5/src/core';
9
9
  import { ContextWatchdog, EditorWatchdog } from 'ckeditor5/src/watchdog';
10
10
  import MultiRootEditorUI from './multirooteditorui';
11
+ import { type RootElement } from 'ckeditor5/src/engine';
11
12
  declare const MultiRootEditor_base: import("ckeditor5/src/utils").Mixed<typeof Editor, import("ckeditor5/src/core").DataApi>;
12
13
  /**
13
14
  * The {@glink installation/getting-started/predefined-builds#multi-root-editor multi-root editor} implementation.
@@ -47,7 +48,12 @@ export default class MultiRootEditor extends MultiRootEditor_base {
47
48
  /**
48
49
  * The elements on which the editor has been initialized.
49
50
  */
50
- readonly sourceElements: Record<string, HTMLElement> | undefined;
51
+ readonly sourceElements: Record<string, HTMLElement>;
52
+ /**
53
+ * Holds attributes keys that were passed in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`}
54
+ * config property and should be returned by {@link #getRootsAttributes}.
55
+ */
56
+ private readonly _registeredRootsAttributesKeys;
51
57
  /**
52
58
  * Creates an instance of the multi-root editor.
53
59
  *
@@ -85,6 +91,170 @@ export default class MultiRootEditor extends MultiRootEditor_base {
85
91
  * ```
86
92
  */
87
93
  destroy(): Promise<unknown>;
94
+ /**
95
+ * Adds a new root to the editor.
96
+ *
97
+ * ```ts
98
+ * editor.addRoot( 'myRoot', { data: '<p>Initial root data.</p>' } );
99
+ * ```
100
+ *
101
+ * After a root is added, you will be able to modify and retrieve its data.
102
+ *
103
+ * All root names must be unique. An error will be thrown if you will try to create a root with the name same as
104
+ * an already existing, attached root. However, you can call this method for a detached root. See also {@link #detachRoot}.
105
+ *
106
+ * Whenever a root is added, the editor instance will fire {@link #event:addRoot `addRoot` event}. The event is also called when
107
+ * the root is added indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
108
+ *
109
+ * Note, that this method only adds a root to the editor model. It **does not** create a DOM editable element for the new root.
110
+ * Until such element is created (and attached to the root), the root is "virtual": it is not displayed anywhere and its data can
111
+ * be changed only using the editor API.
112
+ *
113
+ * To create a DOM editable element for the root, listen to {@link #event:addRoot `addRoot` event} and call {@link #createEditable}.
114
+ * Then, insert the DOM element in a desired place, that will depend on the integration with your application and your requirements.
115
+ *
116
+ * ```ts
117
+ * editor.on( 'addRoot', ( evt, root ) => {
118
+ * const editableElement = editor.createEditable( root );
119
+ *
120
+ * // You may want to create a more complex DOM structure here.
121
+ * //
122
+ * // Alternatively, you may want to create a DOM structure before
123
+ * // calling `editor.addRoot()` and only append `editableElement` at
124
+ * // a proper place.
125
+ *
126
+ * document.querySelector( '#editors' ).appendChild( editableElement );
127
+ * } );
128
+ *
129
+ * // ...
130
+ *
131
+ * editor.addRoot( 'myRoot' ); // Will create a root, a DOM editable element and append it to `#editors` container element.
132
+ * ```
133
+ *
134
+ * You can set root attributes on the new root while you add it:
135
+ *
136
+ * ```ts
137
+ * // Add a collapsed root at fourth position from top.
138
+ * // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes.
139
+ * editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } );
140
+ * ```
141
+ *
142
+ * See also {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes` configuration option}.
143
+ *
144
+ * Note that attributes keys of attributes added in `attributes` option are also included in {@link #getRootsAttributes} return value.
145
+ *
146
+ * By setting `isUndoable` flag to `true`, you can allow for detaching the root using the undo feature.
147
+ *
148
+ * Additionally, you can group adding multiple roots in one undo step. This can be useful if you add multiple roots that are
149
+ * combined into one, bigger UI element, and want them all to be undone together.
150
+ *
151
+ * ```ts
152
+ * let rowId = 0;
153
+ *
154
+ * editor.model.change( () => {
155
+ * editor.addRoot( 'left-row-' + rowId, { isUndoable: true } );
156
+ * editor.addRoot( 'center-row-' + rowId, { isUndoable: true } );
157
+ * editor.addRoot( 'right-row-' + rowId, { isUndoable: true } );
158
+ *
159
+ * rowId++;
160
+ * } );
161
+ * ```
162
+ *
163
+ * @param rootName Name of the root to add.
164
+ * @param options Additional options for the added root.
165
+ */
166
+ addRoot(rootName: string, { data, attributes, elementName, isUndoable }?: AddRootOptions): void;
167
+ /**
168
+ * Detaches a root from the editor.
169
+ *
170
+ * ```ts
171
+ * editor.detachRoot( 'myRoot' );
172
+ * ```
173
+ *
174
+ * A detached root is not entirely removed from the editor model, however it can be considered removed.
175
+ *
176
+ * After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it,
177
+ * it is automatically removed as well. Finally, a detached root is not returned by
178
+ * {@link module:engine/model/document~Document#getRootNames} by default.
179
+ *
180
+ * It is possible to re-add a previously detached root calling {@link #addRoot}.
181
+ *
182
+ * Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also
183
+ * called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
184
+ *
185
+ * Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with
186
+ * the root and it **does not** remove the DOM element from the DOM structure of your application.
187
+ *
188
+ * To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event}
189
+ * and call {@link #detachEditable}. Then, remove the DOM element from your application.
190
+ *
191
+ * ```ts
192
+ * editor.on( 'detachRoot', ( evt, root ) => {
193
+ * const editableElement = editor.detachEditable( root );
194
+ *
195
+ * // You may want to do an additional DOM clean-up here.
196
+ *
197
+ * editableElement.remove();
198
+ * } );
199
+ *
200
+ * // ...
201
+ *
202
+ * editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element.
203
+ * ```
204
+ *
205
+ * By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature.
206
+ *
207
+ * Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one,
208
+ * bigger UI element, and you want them all to be re-added together.
209
+ *
210
+ * ```ts
211
+ * editor.model.change( () => {
212
+ * editor.detachRoot( 'left-row-3', true );
213
+ * editor.detachRoot( 'center-row-3', true );
214
+ * editor.detachRoot( 'right-row-3', true );
215
+ * } );
216
+ * ```
217
+ *
218
+ * @param rootName Name of the root to detach.
219
+ * @param isUndoable Whether detaching the root can be undone (using the undo feature) or not.
220
+ */
221
+ detachRoot(rootName: string, isUndoable?: boolean): void;
222
+ /**
223
+ * Creates and returns a new DOM editable element for the given root element.
224
+ *
225
+ * The new DOM editable is attached to the model root and can be used to modify the root content.
226
+ *
227
+ * @param root Root for which the editable element should be created.
228
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
229
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
230
+ * @returns The created DOM element. Append it in a desired place in your application.
231
+ */
232
+ createEditable(root: RootElement, placeholder?: string): HTMLElement;
233
+ /**
234
+ * Detaches the DOM editable element that was attached to the given root.
235
+ *
236
+ * @param root Root for which the editable element should be detached.
237
+ * @returns The DOM element that was detached. You may want to remove it from your application DOM structure.
238
+ */
239
+ detachEditable(root: RootElement): HTMLElement;
240
+ /**
241
+ * Returns the document data for all attached roots.
242
+ *
243
+ * @param options Additional configuration for the retrieved data.
244
+ * Editor features may introduce more configuration options that can be set through this parameter.
245
+ * @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
246
+ * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
247
+ * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
248
+ * @returns The full document data.
249
+ */
250
+ getFullData(options?: Record<string, unknown>): Record<string, string>;
251
+ /**
252
+ * Returns currently set roots attributes for attributes specified in
253
+ * {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} configuration option.
254
+ *
255
+ * @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root.
256
+ */
257
+ getRootsAttributes(): Record<string, RootAttributes>;
88
258
  /**
89
259
  * Creates a new multi-root editor instance.
90
260
  *
@@ -239,4 +409,61 @@ export default class MultiRootEditor extends MultiRootEditor_base {
239
409
  */
240
410
  static ContextWatchdog: typeof ContextWatchdog;
241
411
  }
412
+ /**
413
+ * Fired whenever a root is {@link ~MultiRootEditor#addRoot added or re-added} to the editor model.
414
+ *
415
+ * Use this event to {@link ~MultiRootEditor#createEditable create a DOM editable} for the added root and append the DOM element
416
+ * in a desired place in your application.
417
+ *
418
+ * 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
419
+ * in the same batch.
420
+ *
421
+ * @eventName ~MultiRootEditor#addRoot
422
+ * @param root The root that was added.
423
+ */
424
+ export type AddRootEvent = {
425
+ name: 'addRoot';
426
+ args: [root: RootElement];
427
+ };
428
+ /**
429
+ * Fired whenever a root is {@link ~MultiRootEditor#detachRoot detached} from the editor model.
430
+ *
431
+ * Use this event to {@link ~MultiRootEditor#detachEditable destroy a DOM editable} for the detached root and remove the DOM element
432
+ * from your application.
433
+ *
434
+ * 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
435
+ * in the same batch.
436
+ *
437
+ * @eventName ~MultiRootEditor#detachRoot
438
+ * @param root The root that was detached.
439
+ */
440
+ export type DetachRootEvent = {
441
+ name: 'detachRoot';
442
+ args: [root: RootElement];
443
+ };
444
+ /**
445
+ * Additional options available when adding a root.
446
+ */
447
+ export type AddRootOptions = {
448
+ /**
449
+ * Initial data for the root.
450
+ */
451
+ data?: string;
452
+ /**
453
+ * Initial attributes for the root.
454
+ */
455
+ attributes?: RootAttributes;
456
+ /**
457
+ * Element name for the root element in the model. It can be used to set different schema rules for different roots.
458
+ */
459
+ elementName?: string;
460
+ /**
461
+ * Whether creating the root can be undone (using the undo feature) or not.
462
+ */
463
+ isUndoable?: boolean;
464
+ };
465
+ /**
466
+ * Attributes set on a model root element.
467
+ */
468
+ export type RootAttributes = Record<string, unknown>;
242
469
  export {};
@@ -62,9 +62,17 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
62
62
  throw new CKEditorError('editor-create-initial-data', null);
63
63
  }
64
64
  super(config);
65
+ /**
66
+ * Holds attributes keys that were passed in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`}
67
+ * config property and should be returned by {@link #getRootsAttributes}.
68
+ */
69
+ this._registeredRootsAttributesKeys = new Set();
65
70
  if (!sourceIsData) {
66
71
  this.sourceElements = sourceElementsOrData;
67
72
  }
73
+ else {
74
+ this.sourceElements = {};
75
+ }
68
76
  if (this.config.get('initialData') === undefined) {
69
77
  // Create initial data object containing data from all roots.
70
78
  const initialData = {};
@@ -82,12 +90,55 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
82
90
  // Create root and `UIView` element for each editable container.
83
91
  this.model.document.createRoot('$root', rootName);
84
92
  }
93
+ if (this.config.get('rootsAttributes')) {
94
+ const rootsAttributes = this.config.get('rootsAttributes');
95
+ for (const [rootName, attributes] of Object.entries(rootsAttributes)) {
96
+ if (!rootNames.includes(rootName)) {
97
+ /**
98
+ * Trying to set attributes on a non-existing root.
99
+ *
100
+ * Roots specified in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes} do not match initial
101
+ * editor roots.
102
+ *
103
+ * @error multi-root-editor-root-attributes-no-root
104
+ */
105
+ throw new CKEditorError('multi-root-editor-root-attributes-no-root', null);
106
+ }
107
+ for (const key of Object.keys(attributes)) {
108
+ this._registeredRootsAttributesKeys.add(key);
109
+ }
110
+ }
111
+ this.data.on('init', () => {
112
+ this.model.enqueueChange({ isUndoable: false }, writer => {
113
+ for (const [name, attributes] of Object.entries(rootsAttributes)) {
114
+ const root = this.model.document.getRoot(name);
115
+ for (const [key, value] of Object.entries(attributes)) {
116
+ if (value !== null) {
117
+ writer.setAttribute(key, value, root);
118
+ }
119
+ }
120
+ }
121
+ });
122
+ });
123
+ }
85
124
  const options = {
86
125
  shouldToolbarGroupWhenFull: !this.config.get('toolbar.shouldNotGroupWhenFull'),
87
126
  editableElements: sourceIsData ? undefined : sourceElementsOrData
88
127
  };
89
128
  const view = new MultiRootEditorUIView(this.locale, this.editing.view, rootNames, options);
90
129
  this.ui = new MultiRootEditorUI(this, view);
130
+ this.model.document.on('change:data', () => {
131
+ const changedRoots = this.model.document.differ.getChangedRoots();
132
+ for (const changes of changedRoots) {
133
+ const root = this.model.document.getRoot(changes.name);
134
+ if (changes.state == 'attached') {
135
+ this.fire('addRoot', root);
136
+ }
137
+ else if (changes.state == 'detached') {
138
+ this.fire('detachRoot', root);
139
+ }
140
+ }
141
+ });
91
142
  }
92
143
  /**
93
144
  * Destroys the editor instance, releasing all resources used by it.
@@ -134,6 +185,224 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
134
185
  }
135
186
  });
136
187
  }
188
+ /**
189
+ * Adds a new root to the editor.
190
+ *
191
+ * ```ts
192
+ * editor.addRoot( 'myRoot', { data: '<p>Initial root data.</p>' } );
193
+ * ```
194
+ *
195
+ * After a root is added, you will be able to modify and retrieve its data.
196
+ *
197
+ * All root names must be unique. An error will be thrown if you will try to create a root with the name same as
198
+ * an already existing, attached root. However, you can call this method for a detached root. See also {@link #detachRoot}.
199
+ *
200
+ * Whenever a root is added, the editor instance will fire {@link #event:addRoot `addRoot` event}. The event is also called when
201
+ * the root is added indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
202
+ *
203
+ * Note, that this method only adds a root to the editor model. It **does not** create a DOM editable element for the new root.
204
+ * Until such element is created (and attached to the root), the root is "virtual": it is not displayed anywhere and its data can
205
+ * be changed only using the editor API.
206
+ *
207
+ * To create a DOM editable element for the root, listen to {@link #event:addRoot `addRoot` event} and call {@link #createEditable}.
208
+ * Then, insert the DOM element in a desired place, that will depend on the integration with your application and your requirements.
209
+ *
210
+ * ```ts
211
+ * editor.on( 'addRoot', ( evt, root ) => {
212
+ * const editableElement = editor.createEditable( root );
213
+ *
214
+ * // You may want to create a more complex DOM structure here.
215
+ * //
216
+ * // Alternatively, you may want to create a DOM structure before
217
+ * // calling `editor.addRoot()` and only append `editableElement` at
218
+ * // a proper place.
219
+ *
220
+ * document.querySelector( '#editors' ).appendChild( editableElement );
221
+ * } );
222
+ *
223
+ * // ...
224
+ *
225
+ * editor.addRoot( 'myRoot' ); // Will create a root, a DOM editable element and append it to `#editors` container element.
226
+ * ```
227
+ *
228
+ * You can set root attributes on the new root while you add it:
229
+ *
230
+ * ```ts
231
+ * // Add a collapsed root at fourth position from top.
232
+ * // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes.
233
+ * editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } );
234
+ * ```
235
+ *
236
+ * See also {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes` configuration option}.
237
+ *
238
+ * Note that attributes keys of attributes added in `attributes` option are also included in {@link #getRootsAttributes} return value.
239
+ *
240
+ * By setting `isUndoable` flag to `true`, you can allow for detaching the root using the undo feature.
241
+ *
242
+ * Additionally, you can group adding multiple roots in one undo step. This can be useful if you add multiple roots that are
243
+ * combined into one, bigger UI element, and want them all to be undone together.
244
+ *
245
+ * ```ts
246
+ * let rowId = 0;
247
+ *
248
+ * editor.model.change( () => {
249
+ * editor.addRoot( 'left-row-' + rowId, { isUndoable: true } );
250
+ * editor.addRoot( 'center-row-' + rowId, { isUndoable: true } );
251
+ * editor.addRoot( 'right-row-' + rowId, { isUndoable: true } );
252
+ *
253
+ * rowId++;
254
+ * } );
255
+ * ```
256
+ *
257
+ * @param rootName Name of the root to add.
258
+ * @param options Additional options for the added root.
259
+ */
260
+ addRoot(rootName, { data = '', attributes = {}, elementName = '$root', isUndoable = false } = {}) {
261
+ const dataController = this.data;
262
+ const registeredKeys = this._registeredRootsAttributesKeys;
263
+ if (isUndoable) {
264
+ this.model.change(_addRoot);
265
+ }
266
+ else {
267
+ this.model.enqueueChange({ isUndoable: false }, _addRoot);
268
+ }
269
+ function _addRoot(writer) {
270
+ const root = writer.addRoot(rootName, elementName);
271
+ if (data) {
272
+ writer.insert(dataController.parse(data, root), root, 0);
273
+ }
274
+ for (const key of Object.keys(attributes)) {
275
+ registeredKeys.add(key);
276
+ writer.setAttribute(key, attributes[key], root);
277
+ }
278
+ }
279
+ }
280
+ /**
281
+ * Detaches a root from the editor.
282
+ *
283
+ * ```ts
284
+ * editor.detachRoot( 'myRoot' );
285
+ * ```
286
+ *
287
+ * A detached root is not entirely removed from the editor model, however it can be considered removed.
288
+ *
289
+ * After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it,
290
+ * it is automatically removed as well. Finally, a detached root is not returned by
291
+ * {@link module:engine/model/document~Document#getRootNames} by default.
292
+ *
293
+ * It is possible to re-add a previously detached root calling {@link #addRoot}.
294
+ *
295
+ * Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also
296
+ * called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
297
+ *
298
+ * Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with
299
+ * the root and it **does not** remove the DOM element from the DOM structure of your application.
300
+ *
301
+ * To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event}
302
+ * and call {@link #detachEditable}. Then, remove the DOM element from your application.
303
+ *
304
+ * ```ts
305
+ * editor.on( 'detachRoot', ( evt, root ) => {
306
+ * const editableElement = editor.detachEditable( root );
307
+ *
308
+ * // You may want to do an additional DOM clean-up here.
309
+ *
310
+ * editableElement.remove();
311
+ * } );
312
+ *
313
+ * // ...
314
+ *
315
+ * editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element.
316
+ * ```
317
+ *
318
+ * By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature.
319
+ *
320
+ * Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one,
321
+ * bigger UI element, and you want them all to be re-added together.
322
+ *
323
+ * ```ts
324
+ * editor.model.change( () => {
325
+ * editor.detachRoot( 'left-row-3', true );
326
+ * editor.detachRoot( 'center-row-3', true );
327
+ * editor.detachRoot( 'right-row-3', true );
328
+ * } );
329
+ * ```
330
+ *
331
+ * @param rootName Name of the root to detach.
332
+ * @param isUndoable Whether detaching the root can be undone (using the undo feature) or not.
333
+ */
334
+ detachRoot(rootName, isUndoable = false) {
335
+ if (isUndoable) {
336
+ this.model.change(writer => writer.detachRoot(rootName));
337
+ }
338
+ else {
339
+ this.model.enqueueChange({ isUndoable: false }, writer => writer.detachRoot(rootName));
340
+ }
341
+ }
342
+ /**
343
+ * Creates and returns a new DOM editable element for the given root element.
344
+ *
345
+ * The new DOM editable is attached to the model root and can be used to modify the root content.
346
+ *
347
+ * @param root Root for which the editable element should be created.
348
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
349
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
350
+ * @returns The created DOM element. Append it in a desired place in your application.
351
+ */
352
+ createEditable(root, placeholder) {
353
+ const editable = this.ui.view.createEditable(root.rootName);
354
+ this.ui.addEditable(editable, placeholder);
355
+ this.editing.view.forceRender();
356
+ return editable.element;
357
+ }
358
+ /**
359
+ * Detaches the DOM editable element that was attached to the given root.
360
+ *
361
+ * @param root Root for which the editable element should be detached.
362
+ * @returns The DOM element that was detached. You may want to remove it from your application DOM structure.
363
+ */
364
+ detachEditable(root) {
365
+ const rootName = root.rootName;
366
+ const editable = this.ui.view.editables[rootName];
367
+ this.ui.removeEditable(editable);
368
+ this.ui.view.removeEditable(rootName);
369
+ return editable.element;
370
+ }
371
+ /**
372
+ * Returns the document data for all attached roots.
373
+ *
374
+ * @param options Additional configuration for the retrieved data.
375
+ * Editor features may introduce more configuration options that can be set through this parameter.
376
+ * @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
377
+ * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
378
+ * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
379
+ * @returns The full document data.
380
+ */
381
+ getFullData(options) {
382
+ const data = {};
383
+ for (const rootName of this.model.document.getRootNames()) {
384
+ data[rootName] = this.data.get({ ...options, rootName });
385
+ }
386
+ return data;
387
+ }
388
+ /**
389
+ * Returns currently set roots attributes for attributes specified in
390
+ * {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} configuration option.
391
+ *
392
+ * @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root.
393
+ */
394
+ getRootsAttributes() {
395
+ const rootsAttributes = {};
396
+ const keys = Array.from(this._registeredRootsAttributesKeys);
397
+ for (const rootName of this.model.document.getRootNames()) {
398
+ rootsAttributes[rootName] = {};
399
+ const root = this.model.document.getRoot(rootName);
400
+ for (const key of keys) {
401
+ rootsAttributes[rootName][key] = root.hasAttribute(key) ? root.getAttribute(key) : null;
402
+ }
403
+ }
404
+ return rootsAttributes;
405
+ }
137
406
  /**
138
407
  * Creates a new multi-root editor instance.
139
408
  *
@@ -6,7 +6,7 @@
6
6
  * @module editor-multi-root/multirooteditorui
7
7
  */
8
8
  import { type Editor } from 'ckeditor5/src/core';
9
- import { EditorUI } from 'ckeditor5/src/ui';
9
+ import { EditorUI, type InlineEditableUIView } from 'ckeditor5/src/ui';
10
10
  import type MultiRootEditorUIView from './multirooteditoruiview';
11
11
  /**
12
12
  * The multi-root editor UI class.
@@ -16,6 +16,10 @@ export default class MultiRootEditorUI extends EditorUI {
16
16
  * The main (top–most) view of the editor UI.
17
17
  */
18
18
  readonly view: MultiRootEditorUIView;
19
+ /**
20
+ * The editable element that was focused the last time when any of the editables had focus.
21
+ */
22
+ private _lastFocusedEditableElement;
19
23
  /**
20
24
  * Creates an instance of the multi-root editor UI class.
21
25
  *
@@ -27,6 +31,30 @@ export default class MultiRootEditorUI extends EditorUI {
27
31
  * Initializes the UI.
28
32
  */
29
33
  init(): void;
34
+ /**
35
+ * Adds the editable to the editor UI.
36
+ *
37
+ * After the editable is added to the editor UI it can be considered "active".
38
+ *
39
+ * The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and
40
+ * changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized.
41
+ *
42
+ * @param editable The editable instance to add.
43
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
44
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
45
+ */
46
+ addEditable(editable: InlineEditableUIView, placeholder?: string): void;
47
+ /**
48
+ * Removes the editable instance from the editor UI.
49
+ *
50
+ * Removed editable can be considered "deactivated".
51
+ *
52
+ * The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in
53
+ * {@link #addEditable} is removed.
54
+ *
55
+ * @param editable Editable to remove from the editor UI.
56
+ */
57
+ removeEditable(editable: InlineEditableUIView): void;
30
58
  /**
31
59
  * @inheritDoc
32
60
  */
@@ -36,7 +64,11 @@ export default class MultiRootEditorUI extends EditorUI {
36
64
  */
37
65
  private _initToolbar;
38
66
  /**
39
- * Enable the placeholder text on the editing roots, if any was configured.
67
+ * Enables the placeholder text on a given editable, if the placeholder was configured.
68
+ *
69
+ * @param editable Editable on which the placeholder should be set.
70
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
71
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
40
72
  */
41
73
  private _initPlaceholder;
42
74
  }
@@ -17,18 +17,13 @@ export default class MultiRootEditorUI extends EditorUI {
17
17
  constructor(editor, view) {
18
18
  super(editor);
19
19
  this.view = view;
20
+ this._lastFocusedEditableElement = null;
20
21
  }
21
22
  /**
22
23
  * Initializes the UI.
23
24
  */
24
25
  init() {
25
26
  const view = this.view;
26
- const editor = this.editor;
27
- const editingView = editor.editing.view;
28
- let lastFocusedEditableElement;
29
- for (const editableName of Object.keys(view.editables)) {
30
- view.editables[editableName].name = editableName;
31
- }
32
27
  view.render();
33
28
  // Keep track of the last focused editable element. Knowing which one was focused
34
29
  // is useful when the focus moves from editable to other UI components like balloons
@@ -38,7 +33,7 @@ export default class MultiRootEditorUI extends EditorUI {
38
33
  this.focusTracker.on('change:focusedElement', (evt, name, focusedElement) => {
39
34
  for (const editable of Object.values(this.view.editables)) {
40
35
  if (focusedElement === editable.element) {
41
- lastFocusedEditableElement = editable.element;
36
+ this._lastFocusedEditableElement = editable.element;
42
37
  }
43
38
  }
44
39
  });
@@ -48,62 +43,90 @@ export default class MultiRootEditorUI extends EditorUI {
48
43
  // to another within the editor UI.
49
44
  this.focusTracker.on('change:isFocused', (evt, name, isFocused) => {
50
45
  if (!isFocused) {
51
- lastFocusedEditableElement = null;
46
+ this._lastFocusedEditableElement = null;
52
47
  }
53
48
  });
54
49
  for (const editable of Object.values(this.view.editables)) {
55
- // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
56
- // But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`.
57
- const editableElement = editable.element;
58
- // Register each editable UI view in the editor.
59
- this.setEditableElement(editable.name, editableElement);
60
- // Let the editable UI element respond to the changes in the global editor focus
61
- // tracker. It has been added to the same tracker a few lines above but, in reality, there are
62
- // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
63
- // as they have focus, the editable should act like it is focused too (although technically
64
- // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
65
- // Doing otherwise will result in editable focus styles disappearing, once e.g. the
66
- // toolbar gets focused.
67
- editable.bind('isFocused').to(this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement', (isFocused, focusedElement) => {
68
- // When the focus tracker is blurred, it means the focus moved out of the editor UI.
69
- // No editable will maintain focus then.
70
- if (!isFocused) {
71
- return false;
72
- }
73
- // If the focus tracker says the editor UI is focused and currently focused element
74
- // is the editable, then the editable should be visually marked as focused too.
75
- if (focusedElement === editableElement) {
76
- return true;
77
- }
78
- // If the focus tracker says the editor UI is focused but the focused element is
79
- // not an editable, it is possible that the editable is still (context–)focused.
80
- // For instance, the focused element could be an input inside of a balloon attached
81
- // to the content in the editable. In such case, the editable should remain _visually_
82
- // focused even though technically the focus is somewhere else. The focus moved from
83
- // the editable to the input but the focus context remained the same.
84
- else {
85
- return lastFocusedEditableElement === editableElement;
86
- }
87
- });
88
- // Bind the editable UI element to the editing view, making it an end– and entry–point
89
- // of the editor's engine. This is where the engine meets the UI.
90
- editingView.attachDomRoot(editableElement, editable.name);
50
+ this.addEditable(editable);
91
51
  }
92
- this._initPlaceholder();
93
52
  this._initToolbar();
94
53
  this.fire('ready');
95
54
  }
55
+ /**
56
+ * Adds the editable to the editor UI.
57
+ *
58
+ * After the editable is added to the editor UI it can be considered "active".
59
+ *
60
+ * The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and
61
+ * changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized.
62
+ *
63
+ * @param editable The editable instance to add.
64
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
65
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
66
+ */
67
+ addEditable(editable, placeholder) {
68
+ // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
69
+ // But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`.
70
+ const editableElement = editable.element;
71
+ // Bind the editable UI element to the editing view, making it an end– and entry–point
72
+ // of the editor's engine. This is where the engine meets the UI.
73
+ this.editor.editing.view.attachDomRoot(editableElement, editable.name);
74
+ // Register each editable UI view in the editor.
75
+ this.setEditableElement(editable.name, editableElement);
76
+ // Let the editable UI element respond to the changes in the global editor focus
77
+ // tracker. It has been added to the same tracker a few lines above but, in reality, there are
78
+ // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
79
+ // as they have focus, the editable should act like it is focused too (although technically
80
+ // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
81
+ // Doing otherwise will result in editable focus styles disappearing, once e.g. the
82
+ // toolbar gets focused.
83
+ editable.bind('isFocused').to(this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement', (isFocused, focusedElement) => {
84
+ // When the focus tracker is blurred, it means the focus moved out of the editor UI.
85
+ // No editable will maintain focus then.
86
+ if (!isFocused) {
87
+ return false;
88
+ }
89
+ // If the focus tracker says the editor UI is focused and currently focused element
90
+ // is the editable, then the editable should be visually marked as focused too.
91
+ if (focusedElement === editableElement) {
92
+ return true;
93
+ }
94
+ // If the focus tracker says the editor UI is focused but the focused element is
95
+ // not an editable, it is possible that the editable is still (context–)focused.
96
+ // For instance, the focused element could be an input inside of a balloon attached
97
+ // to the content in the editable. In such case, the editable should remain _visually_
98
+ // focused even though technically the focus is somewhere else. The focus moved from
99
+ // the editable to the input but the focus context remained the same.
100
+ else {
101
+ return this._lastFocusedEditableElement === editableElement;
102
+ }
103
+ });
104
+ this._initPlaceholder(editable, placeholder);
105
+ }
106
+ /**
107
+ * Removes the editable instance from the editor UI.
108
+ *
109
+ * Removed editable can be considered "deactivated".
110
+ *
111
+ * The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in
112
+ * {@link #addEditable} is removed.
113
+ *
114
+ * @param editable Editable to remove from the editor UI.
115
+ */
116
+ removeEditable(editable) {
117
+ this.editor.editing.view.detachDomRoot(editable.name);
118
+ editable.unbind('isFocused');
119
+ this.removeEditableElement(editable.name);
120
+ }
96
121
  /**
97
122
  * @inheritDoc
98
123
  */
99
124
  destroy() {
100
125
  super.destroy();
101
- const view = this.view;
102
- const editingView = this.editor.editing.view;
103
126
  for (const editable of Object.values(this.view.editables)) {
104
- editingView.detachDomRoot(editable.name);
127
+ this.removeEditable(editable);
105
128
  }
106
- view.destroy();
129
+ this.view.destroy();
107
130
  }
108
131
  /**
109
132
  * Initializes the editor main toolbar and its panel.
@@ -113,31 +136,34 @@ export default class MultiRootEditorUI extends EditorUI {
113
136
  const view = this.view;
114
137
  const toolbar = view.toolbar;
115
138
  toolbar.fillFromConfig(editor.config.get('toolbar'), this.componentFactory);
116
- // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
139
+ // Register the toolbar, so it becomes available for Alt+F10 and Esc navigation.
117
140
  this.addToolbar(view.toolbar);
118
141
  }
119
142
  /**
120
- * Enable the placeholder text on the editing roots, if any was configured.
143
+ * Enables the placeholder text on a given editable, if the placeholder was configured.
144
+ *
145
+ * @param editable Editable on which the placeholder should be set.
146
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
147
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
121
148
  */
122
- _initPlaceholder() {
123
- const editor = this.editor;
124
- const editingView = editor.editing.view;
125
- const placeholder = editor.config.get('placeholder');
149
+ _initPlaceholder(editable, placeholder) {
126
150
  if (!placeholder) {
127
- return;
128
- }
129
- for (const editable of Object.values(this.view.editables)) {
130
- const editingRoot = editingView.document.getRoot(editable.name);
131
- const placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[editable.name];
132
- if (placeholderText) {
133
- enablePlaceholder({
134
- view: editingView,
135
- element: editingRoot,
136
- text: placeholderText,
137
- isDirectHost: false,
138
- keepOnFocus: true
139
- });
151
+ const configPlaceholder = this.editor.config.get('placeholder');
152
+ if (configPlaceholder) {
153
+ placeholder = typeof configPlaceholder === 'string' ? configPlaceholder : configPlaceholder[editable.name];
140
154
  }
141
155
  }
156
+ if (!placeholder) {
157
+ return;
158
+ }
159
+ const editingView = this.editor.editing.view;
160
+ const editingRoot = editingView.document.getRoot(editable.name);
161
+ enablePlaceholder({
162
+ view: editingView,
163
+ element: editingRoot,
164
+ text: placeholder,
165
+ isDirectHost: false,
166
+ keepOnFocus: true
167
+ });
142
168
  }
143
169
  }
@@ -27,6 +27,10 @@ export default class MultiRootEditorUIView extends EditorUIView {
27
27
  */
28
28
  readonly editables: Record<string, InlineEditableUIView>;
29
29
  readonly editable: InlineEditableUIView;
30
+ /**
31
+ * The editing view instance this view is related to.
32
+ */
33
+ private readonly _editingView;
30
34
  /**
31
35
  * Creates an instance of the multi-root editor UI view.
32
36
  *
@@ -46,6 +50,23 @@ export default class MultiRootEditorUIView extends EditorUIView {
46
50
  editableElements?: Record<string, HTMLElement>;
47
51
  shouldToolbarGroupWhenFull?: boolean;
48
52
  });
53
+ /**
54
+ * Creates an editable instance with given name and registers it in the editor UI view.
55
+ *
56
+ * If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new
57
+ * DOM element and use it instead.
58
+ *
59
+ * @param editableName The name for the editable.
60
+ * @param editableElement DOM element for which the editable should be created.
61
+ * @returns The created editable instance.
62
+ */
63
+ createEditable(editableName: string, editableElement?: HTMLElement): InlineEditableUIView;
64
+ /**
65
+ * Destroys and removes the editable from the editor UI view.
66
+ *
67
+ * @param editableName The name of the editable that should be removed.
68
+ */
69
+ removeEditable(editableName: string): void;
49
70
  /**
50
71
  * @inheritDoc
51
72
  */
@@ -33,19 +33,15 @@ export default class MultiRootEditorUIView extends EditorUIView {
33
33
  */
34
34
  constructor(locale, editingView, editableNames, options = {}) {
35
35
  super(locale);
36
- const t = locale.t;
36
+ this._editingView = editingView;
37
37
  this.toolbar = new ToolbarView(locale, {
38
38
  shouldGroupWhenFull: options.shouldToolbarGroupWhenFull
39
39
  });
40
40
  this.editables = {};
41
41
  // Create `InlineEditableUIView` instance for each editable.
42
42
  for (const editableName of editableNames) {
43
- const editable = new InlineEditableUIView(locale, editingView, options.editableElements ? options.editableElements[editableName] : undefined, {
44
- label: editable => {
45
- return t('Rich Text Editor. Editing area: %0', editable.name);
46
- }
47
- });
48
- this.editables[editableName] = editable;
43
+ const editableElement = options.editableElements ? options.editableElements[editableName] : undefined;
44
+ this.createEditable(editableName, editableElement);
49
45
  }
50
46
  this.editable = Object.values(this.editables)[0];
51
47
  // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
@@ -62,12 +58,49 @@ export default class MultiRootEditorUIView extends EditorUIView {
62
58
  }
63
59
  });
64
60
  }
61
+ /**
62
+ * Creates an editable instance with given name and registers it in the editor UI view.
63
+ *
64
+ * If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new
65
+ * DOM element and use it instead.
66
+ *
67
+ * @param editableName The name for the editable.
68
+ * @param editableElement DOM element for which the editable should be created.
69
+ * @returns The created editable instance.
70
+ */
71
+ createEditable(editableName, editableElement) {
72
+ const t = this.locale.t;
73
+ const editable = new InlineEditableUIView(this.locale, this._editingView, editableElement, {
74
+ label: editable => {
75
+ return t('Rich Text Editor. Editing area: %0', editable.name);
76
+ }
77
+ });
78
+ this.editables[editableName] = editable;
79
+ editable.name = editableName;
80
+ if (this.isRendered) {
81
+ this.registerChild(editable);
82
+ }
83
+ return editable;
84
+ }
85
+ /**
86
+ * Destroys and removes the editable from the editor UI view.
87
+ *
88
+ * @param editableName The name of the editable that should be removed.
89
+ */
90
+ removeEditable(editableName) {
91
+ const editable = this.editables[editableName];
92
+ if (this.isRendered) {
93
+ this.deregisterChild(editable);
94
+ }
95
+ delete this.editables[editableName];
96
+ editable.destroy();
97
+ }
65
98
  /**
66
99
  * @inheritDoc
67
100
  */
68
101
  render() {
69
102
  super.render();
70
103
  this.registerChild(Object.values(this.editables));
71
- this.registerChild([this.toolbar]);
104
+ this.registerChild(this.toolbar);
72
105
  }
73
106
  }