-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
multirooteditorui.ts
222 lines (189 loc) · 7.79 KB
/
multirooteditorui.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module editor-multi-root/multirooteditorui
*/
import {
type Editor
} from 'ckeditor5/src/core';
import {
EditorUI,
type EditorUIReadyEvent,
type InlineEditableUIView
} from 'ckeditor5/src/ui';
import { enablePlaceholder } from 'ckeditor5/src/engine';
import type MultiRootEditorUIView from './multirooteditoruiview';
/**
* The multi-root editor UI class.
*/
export default class MultiRootEditorUI extends EditorUI {
/**
* The main (top–most) view of the editor UI.
*/
public readonly view: MultiRootEditorUIView;
/**
* The editable element that was focused the last time when any of the editables had focus.
*/
private _lastFocusedEditableElement: HTMLElement | null;
/**
* Creates an instance of the multi-root editor UI class.
*
* @param editor The editor instance.
* @param view The view of the UI.
*/
constructor( editor: Editor, view: MultiRootEditorUIView ) {
super( editor );
this.view = view;
this._lastFocusedEditableElement = null;
}
/**
* Initializes the UI.
*/
public init(): void {
const view = this.view;
view.render();
// Keep track of the last focused editable element. Knowing which one was focused
// is useful when the focus moves from editable to other UI components like balloons
// (especially inputs) but the editable remains the "focus context" (e.g. link balloon
// attached to a link in an editable). In this case, the editable should preserve visual
// focus styles.
this.focusTracker.on( 'change:focusedElement', ( evt, name, focusedElement ) => {
for ( const editable of Object.values( this.view.editables ) ) {
if ( focusedElement === editable.element ) {
this._lastFocusedEditableElement = editable.element;
}
}
} );
// If the focus tracker loses focus, stop tracking the last focused editable element.
// Wherever the focus is restored, it will no longer be in the context of that editable
// because the focus "came from the outside", as opposed to the focus moving from one element
// to another within the editor UI.
this.focusTracker.on( 'change:isFocused', ( evt, name, isFocused ) => {
if ( !isFocused ) {
this._lastFocusedEditableElement = null;
}
} );
for ( const editable of Object.values( this.view.editables ) ) {
this.addEditable( editable );
}
this._initToolbar();
this.fire<EditorUIReadyEvent>( 'ready' );
}
/**
* Adds the editable to the editor UI.
*
* After the editable is added to the editor UI it can be considered "active".
*
* The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and
* changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized.
*
* @param editable The editable instance to add.
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
*/
public addEditable( editable: InlineEditableUIView, placeholder?: string ): void {
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
// But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`.
const editableElement = editable.element!;
// Bind the editable UI element to the editing view, making it an end– and entry–point
// of the editor's engine. This is where the engine meets the UI.
this.editor.editing.view.attachDomRoot( editableElement, editable.name! );
// Register each editable UI view in the editor.
this.setEditableElement( editable.name!, editableElement );
// Let the editable UI element respond to the changes in the global editor focus
// tracker. It has been added to the same tracker a few lines above but, in reality, there are
// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
// as they have focus, the editable should act like it is focused too (although technically
// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
// Doing otherwise will result in editable focus styles disappearing, once e.g. the
// toolbar gets focused.
editable.bind( 'isFocused' ).to( this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement',
( isFocused: boolean, focusedElement: Element | null ) => {
// When the focus tracker is blurred, it means the focus moved out of the editor UI.
// No editable will maintain focus then.
if ( !isFocused ) {
return false;
}
// If the focus tracker says the editor UI is focused and currently focused element
// is the editable, then the editable should be visually marked as focused too.
if ( focusedElement === editableElement ) {
return true;
}
// If the focus tracker says the editor UI is focused but the focused element is
// not an editable, it is possible that the editable is still (context–)focused.
// For instance, the focused element could be an input inside of a balloon attached
// to the content in the editable. In such case, the editable should remain _visually_
// focused even though technically the focus is somewhere else. The focus moved from
// the editable to the input but the focus context remained the same.
else {
return this._lastFocusedEditableElement === editableElement;
}
} );
this._initPlaceholder( editable, placeholder );
}
/**
* Removes the editable instance from the editor UI.
*
* Removed editable can be considered "deactivated".
*
* The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in
* {@link #addEditable} is removed.
*
* @param editable Editable to remove from the editor UI.
*/
public removeEditable( editable: InlineEditableUIView ): void {
this.editor.editing.view.detachDomRoot( editable.name! );
editable.unbind( 'isFocused' );
this.removeEditableElement( editable.name! );
}
/**
* @inheritDoc
*/
public override destroy(): void {
super.destroy();
for ( const editable of Object.values( this.view.editables ) ) {
this.removeEditable( editable );
}
this.view.destroy();
}
/**
* Initializes the editor main toolbar and its panel.
*/
private _initToolbar(): void {
const editor = this.editor;
const view = this.view;
const toolbar = view.toolbar;
toolbar.fillFromConfig( editor.config.get( 'toolbar' ), this.componentFactory );
// Register the toolbar, so it becomes available for Alt+F10 and Esc navigation.
this.addToolbar( view.toolbar );
}
/**
* Enables the placeholder text on a given editable, if the placeholder was configured.
*
* @param editable Editable on which the placeholder should be set.
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
*/
private _initPlaceholder( editable: InlineEditableUIView, placeholder?: string ): void {
if ( !placeholder ) {
const configPlaceholder = this.editor.config.get( 'placeholder' );
if ( configPlaceholder ) {
placeholder = typeof configPlaceholder === 'string' ? configPlaceholder : configPlaceholder[ editable.name! ];
}
}
if ( !placeholder ) {
return;
}
const editingView = this.editor.editing.view;
const editingRoot = editingView.document.getRoot( editable.name! )!;
enablePlaceholder( {
view: editingView,
element: editingRoot,
text: placeholder,
isDirectHost: false,
keepOnFocus: true
} );
}
}