@ckeditor/ckeditor5-editor-multi-root 37.0.0-alpha.2 → 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.
- package/build/editor-multi-root.js +1 -1
- package/package.json +16 -15
- package/src/augmentation.d.ts +80 -0
- package/src/augmentation.js +5 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/multirooteditor.d.ts +248 -2
- package/src/multirooteditor.js +289 -1
- package/src/multirooteditorui.d.ts +34 -2
- package/src/multirooteditorui.js +94 -68
- package/src/multirooteditoruiview.d.ts +21 -0
- package/src/multirooteditoruiview.js +41 -8
@@ -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")},79:t=>{"use strict";t.exports=CKEditor5.dll}},e={};function o(i){var
|
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-
|
3
|
+
"version": "37.0.0-rc.0",
|
4
4
|
"description": "Multi-root editor implementation for CKEditor 5.",
|
5
5
|
"keywords": [
|
6
6
|
"ckeditor",
|
@@ -11,28 +11,29 @@
|
|
11
11
|
],
|
12
12
|
"main": "src/index.js",
|
13
13
|
"dependencies": {
|
14
|
-
"ckeditor5": "^37.0.0-
|
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-
|
19
|
-
"@ckeditor/ckeditor5-core": "^37.0.0-
|
20
|
-
"@ckeditor/ckeditor5-dev-utils": "^
|
21
|
-
"@ckeditor/ckeditor5-engine": "^37.0.0-
|
22
|
-
"@ckeditor/ckeditor5-enter": "^37.0.0-
|
23
|
-
"@ckeditor/ckeditor5-heading": "^37.0.0-
|
24
|
-
"@ckeditor/ckeditor5-paragraph": "^37.0.0-
|
25
|
-
"@ckeditor/ckeditor5-theme-lark": "^37.0.0-
|
26
|
-
"@ckeditor/ckeditor5-typing": "^37.0.0-
|
27
|
-
"@ckeditor/ckeditor5-ui": "^37.0.0-
|
28
|
-
"@ckeditor/ckeditor5-undo": "^37.0.0-
|
29
|
-
"@ckeditor/ckeditor5-utils": "^37.0.0-
|
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",
|
30
31
|
"typescript": "^4.8.4",
|
31
32
|
"webpack": "^5.58.1",
|
32
33
|
"webpack-cli": "^4.9.0"
|
33
34
|
},
|
34
35
|
"engines": {
|
35
|
-
"node": ">=
|
36
|
+
"node": ">=16.0.0",
|
36
37
|
"npm": ">=5.7.1"
|
37
38
|
},
|
38
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
|
+
}
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
package/src/multirooteditor.d.ts
CHANGED
@@ -5,8 +5,10 @@
|
|
5
5
|
/**
|
6
6
|
* @module editor-multi-root/multirooteditor
|
7
7
|
*/
|
8
|
-
import { Editor, type EditorConfig } from 'ckeditor5/src/core';
|
8
|
+
import { Editor, Context, type EditorConfig } from 'ckeditor5/src/core';
|
9
|
+
import { ContextWatchdog, EditorWatchdog } from 'ckeditor5/src/watchdog';
|
9
10
|
import MultiRootEditorUI from './multirooteditorui';
|
11
|
+
import { type RootElement } from 'ckeditor5/src/engine';
|
10
12
|
declare const MultiRootEditor_base: import("ckeditor5/src/utils").Mixed<typeof Editor, import("ckeditor5/src/core").DataApi>;
|
11
13
|
/**
|
12
14
|
* The {@glink installation/getting-started/predefined-builds#multi-root-editor multi-root editor} implementation.
|
@@ -46,7 +48,12 @@ export default class MultiRootEditor extends MultiRootEditor_base {
|
|
46
48
|
/**
|
47
49
|
* The elements on which the editor has been initialized.
|
48
50
|
*/
|
49
|
-
readonly sourceElements: Record<string, HTMLElement
|
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;
|
50
57
|
/**
|
51
58
|
* Creates an instance of the multi-root editor.
|
52
59
|
*
|
@@ -84,6 +91,170 @@ export default class MultiRootEditor extends MultiRootEditor_base {
|
|
84
91
|
* ```
|
85
92
|
*/
|
86
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> </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>;
|
87
258
|
/**
|
88
259
|
* Creates a new multi-root editor instance.
|
89
260
|
*
|
@@ -219,5 +390,80 @@ export default class MultiRootEditor extends MultiRootEditor_base {
|
|
219
390
|
* @returns A promise resolved once the editor is ready. The promise resolves with the created editor instance.
|
220
391
|
*/
|
221
392
|
static create(sourceElementsOrData: Record<string, HTMLElement> | Record<string, string>, config?: EditorConfig): Promise<MultiRootEditor>;
|
393
|
+
/**
|
394
|
+
* The {@link module:core/context~Context} class.
|
395
|
+
*
|
396
|
+
* Exposed as static editor field for easier access in editor builds.
|
397
|
+
*/
|
398
|
+
static Context: typeof Context;
|
399
|
+
/**
|
400
|
+
* The {@link module:watchdog/editorwatchdog~EditorWatchdog} class.
|
401
|
+
*
|
402
|
+
* Exposed as static editor field for easier access in editor builds.
|
403
|
+
*/
|
404
|
+
static EditorWatchdog: typeof EditorWatchdog;
|
405
|
+
/**
|
406
|
+
* The {@link module:watchdog/contextwatchdog~ContextWatchdog} class.
|
407
|
+
*
|
408
|
+
* Exposed as static editor field for easier access in editor builds.
|
409
|
+
*/
|
410
|
+
static ContextWatchdog: typeof ContextWatchdog;
|
222
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>;
|
223
469
|
export {};
|
package/src/multirooteditor.js
CHANGED
@@ -5,8 +5,9 @@
|
|
5
5
|
/**
|
6
6
|
* @module editor-multi-root/multirooteditor
|
7
7
|
*/
|
8
|
-
import { Editor, DataApiMixin, secureSourceElement } from 'ckeditor5/src/core';
|
8
|
+
import { Editor, Context, DataApiMixin, secureSourceElement } from 'ckeditor5/src/core';
|
9
9
|
import { CKEditorError, getDataFromElement, setDataInElement } from 'ckeditor5/src/utils';
|
10
|
+
import { ContextWatchdog, EditorWatchdog } from 'ckeditor5/src/watchdog';
|
10
11
|
import MultiRootEditorUI from './multirooteditorui';
|
11
12
|
import MultiRootEditorUIView from './multirooteditoruiview';
|
12
13
|
import { isElement as _isElement } from 'lodash-es';
|
@@ -61,9 +62,17 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
|
|
61
62
|
throw new CKEditorError('editor-create-initial-data', null);
|
62
63
|
}
|
63
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();
|
64
70
|
if (!sourceIsData) {
|
65
71
|
this.sourceElements = sourceElementsOrData;
|
66
72
|
}
|
73
|
+
else {
|
74
|
+
this.sourceElements = {};
|
75
|
+
}
|
67
76
|
if (this.config.get('initialData') === undefined) {
|
68
77
|
// Create initial data object containing data from all roots.
|
69
78
|
const initialData = {};
|
@@ -81,12 +90,55 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
|
|
81
90
|
// Create root and `UIView` element for each editable container.
|
82
91
|
this.model.document.createRoot('$root', rootName);
|
83
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
|
+
}
|
84
124
|
const options = {
|
85
125
|
shouldToolbarGroupWhenFull: !this.config.get('toolbar.shouldNotGroupWhenFull'),
|
86
126
|
editableElements: sourceIsData ? undefined : sourceElementsOrData
|
87
127
|
};
|
88
128
|
const view = new MultiRootEditorUIView(this.locale, this.editing.view, rootNames, options);
|
89
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
|
+
});
|
90
142
|
}
|
91
143
|
/**
|
92
144
|
* Destroys the editor instance, releasing all resources used by it.
|
@@ -133,6 +185,224 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
|
|
133
185
|
}
|
134
186
|
});
|
135
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> </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
|
+
}
|
136
406
|
/**
|
137
407
|
* Creates a new multi-root editor instance.
|
138
408
|
*
|
@@ -285,6 +555,24 @@ export default class MultiRootEditor extends DataApiMixin(Editor) {
|
|
285
555
|
});
|
286
556
|
}
|
287
557
|
}
|
558
|
+
/**
|
559
|
+
* The {@link module:core/context~Context} class.
|
560
|
+
*
|
561
|
+
* Exposed as static editor field for easier access in editor builds.
|
562
|
+
*/
|
563
|
+
MultiRootEditor.Context = Context;
|
564
|
+
/**
|
565
|
+
* The {@link module:watchdog/editorwatchdog~EditorWatchdog} class.
|
566
|
+
*
|
567
|
+
* Exposed as static editor field for easier access in editor builds.
|
568
|
+
*/
|
569
|
+
MultiRootEditor.EditorWatchdog = EditorWatchdog;
|
570
|
+
/**
|
571
|
+
* The {@link module:watchdog/contextwatchdog~ContextWatchdog} class.
|
572
|
+
*
|
573
|
+
* Exposed as static editor field for easier access in editor builds.
|
574
|
+
*/
|
575
|
+
MultiRootEditor.ContextWatchdog = ContextWatchdog;
|
288
576
|
function getInitialData(sourceElementOrData) {
|
289
577
|
return isElement(sourceElementOrData) ? getDataFromElement(sourceElementOrData) : sourceElementOrData;
|
290
578
|
}
|
@@ -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
|
-
*
|
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
|
}
|
package/src/multirooteditorui.js
CHANGED
@@ -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
|
-
|
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
|
-
|
46
|
+
this._lastFocusedEditableElement = null;
|
52
47
|
}
|
53
48
|
});
|
54
49
|
for (const editable of Object.values(this.view.editables)) {
|
55
|
-
|
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
|
-
|
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
|
-
*
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
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
|
44
|
-
|
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(
|
104
|
+
this.registerChild(this.toolbar);
|
72
105
|
}
|
73
106
|
}
|