/
bootstrap-ui-inner.js
332 lines (258 loc) · 11.7 KB
/
bootstrap-ui-inner.js
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* globals $, window, console:false */
// Basic classes to create an editor.
import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
// Interfaces to extend basic Editor API.
import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
import ElementApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin';
// Helper function for adding interfaces to the Editor class.
import mix from '@ckeditor/ckeditor5-utils/src/mix';
// Helper function that gets data from HTML element that the Editor is attached to.
import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
// Helper function that binds editor with HTMLForm element.
import attachToForm from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform';
// Basic features that every editor should enable.
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
// Basic features to associated with the edited content.
import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
import UnderlineEditing from '@ckeditor/ckeditor5-basic-styles/src/underline/underlineediting';
import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
// The easy image integration.
import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices';
import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
// Extending the Editor class, which brings base editor API.
export default class BootstrapEditor extends Editor {
constructor( element, config ) {
super( config );
// Remember the element the editor is created with.
this.sourceElement = element;
// Create the ("main") root element of the model tree.
this.model.document.createRoot();
// The UI layer of the editor.
this.ui = new BootstrapEditorUI( this );
// When editor#element is a textarea inside a form element
// then content of this textarea will be updated on form submit.
attachToForm( this );
}
destroy() {
// When destroyed, editor sets the output of editor#getData() into editor#element...
this.updateSourceElement();
// ...and destroys the UI.
this.ui.destroy();
return super.destroy();
}
static create( element, config ) {
return new Promise( resolve => {
const editor = new this( element, config );
resolve(
editor.initPlugins()
// Initialize the UI first. See the BootstrapEditorUI class to learn more.
.then( () => editor.ui.init( element ) )
// Fill the editable with the initial data.
.then( () => editor.data.init( getDataFromElement( element ) ) )
// Fire the `editor#ready` event that announce the editor is complete and ready to use.
.then( () => editor.fire( 'ready' ) )
.then( () => editor )
);
} );
}
}
// Mixing interfaces, which extends basic editor API.
mix( BootstrapEditor, DataApiMixin );
mix( BootstrapEditor, ElementApiMixin );
// The class organizing the UI of the editor, binding it with existing Bootstrap elements in DOM.
class BootstrapEditorUI extends EditorUI {
constructor( editor ) {
super( editor );
// A helper to easily replace the editor#element with editor.editable#element.
this._elementReplacer = new ElementReplacer();
// The global UI view of the editor. It aggregates various Bootstrap DOM elements.
const view = this._view = new EditorUIView( editor.locale );
// This is the main editor element in the DOM.
view.element = $( '.ck-editor' );
// This is the editable view in the DOM. It will replace the data container in the DOM.
view.editable = new InlineEditableUIView( editor.locale, editor.editing.view );
// References to the dropdown elements for further usage. See #_setupBootstrapHeadingDropdown.
view.dropdownMenu = view.element.find( '.dropdown-menu' );
view.dropdownToggle = view.element.find( '.dropdown-toggle' );
// References to the toolbar buttons for further usage. See #_setupBootstrapToolbarButtons.
view.toolbarButtons = {};
[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( name => {
// Retrieve the jQuery object corresponding with the button in the DOM.
view.toolbarButtons[ name ] = view.element.find( `#${ name }` );
} );
}
// All EditorUI subclasses should expose their view instance
// so other UI classes can access it if necessary.
get view() {
return this._view;
}
init( replacementElement ) {
const editor = this.editor;
const view = this.view;
const editingView = editor.editing.view;
// Make sure the EditorUIView is rendered. This will, for instance, create a place for UI elements
// like floating panels detached from the main editor UI in DOM.
this._view.render();
// Create an editing root in the editing layer. It will correspond with the
// document root created in the constructor().
const editingRoot = editingView.document.getRoot();
// The editable UI and editing root should share the same name.
view.editable.name = editingRoot.rootName;
// Render the editable component in the DOM first.
view.editable.render();
const editableElement = view.editable.element;
// Register editable element so it is available via getEditableElement() method.
this.setEditableElement( view.editable.name, editableElement );
view.editable.bind( 'isFocused' ).to( this.focusTracker );
// Bind the editable UI element to the editing view, making it an end– and entry–point
// of the editor's engine. This is where the engine meets the UI.
editingView.attachDomRoot( editableElement );
// Setup the existing, external Bootstrap UI so it works with the rest of the editor.
this._setupBootstrapToolbarButtons();
this._setupBootstrapHeadingDropdown();
// Replace the editor#element with editor.editable#element.
this._elementReplacer.replace( replacementElement, editableElement );
// Tell the world that the UI of the editor is ready to use.
this.fire( 'ready' );
}
destroy() {
super.destroy();
// Restore the original editor#element.
this._elementReplacer.restore();
// Destroy the view.
this._view.editable.destroy();
this._view.destroy();
}
// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
_setupBootstrapToolbarButtons() {
const editor = this.editor;
for ( const name in this.view.toolbarButtons ) {
// Retrieve the editor command corresponding with the id of the button in DOM.
const command = editor.commands.get( name );
const button = this.view.toolbarButtons[ name ];
// Clicking on the buttons should execute the editor command...
button.click( () => editor.execute( name ) );
// ...but it should not steal the focus so the editing is uninterrupted.
button.mousedown( evt => evt.preventDefault() );
const onValueChange = () => {
button.toggleClass( 'active', command.value );
};
const onIsEnabledChange = () => {
button.attr( 'disabled', () => !command.isEnabled );
};
// Commands can become disabled, e.g. when the editor is read-only.
// Make sure the buttons reflect this state change.
command.on( 'change:isEnabled', onIsEnabledChange );
onIsEnabledChange();
// Bold, Italic and Underline commands have a value that changes
// when the selection starts in an element the command creates.
// The button should indicate that e.g. editing text which is already bold.
if ( !new Set( [ 'undo', 'redo' ] ).has( name ) ) {
command.on( 'change:value', onValueChange );
onValueChange();
}
}
}
// This method activates the headings dropdown in the toolbar.
_setupBootstrapHeadingDropdown() {
const editor = this.editor;
const dropdownMenu = this.view.dropdownMenu;
const dropdownToggle = this.view.dropdownToggle;
// Retrieve the editor commands for heading and paragraph.
const headingCommand = editor.commands.get( 'heading' );
const paragraphCommand = editor.commands.get( 'paragraph' );
// Create a dropdown menu entry for each heading configuration option.
editor.config.get( 'heading.options' ).map( option => {
// Check is options is paragraph or heading as their commands slightly differ.
const isParagraph = option.model === 'paragraph';
// Create the menu item DOM element.
const menuItem = $(
`<a href="#" class="dropdown-item heading-item_${ option.model }">` +
`${ option.title }` +
'</a>'
);
// Upon click, the dropdown menu item should execute the command and focus
// the editing view to keep the editing process uninterrupted.
menuItem.click( () => {
const commandName = isParagraph ? 'paragraph' : 'heading';
const commandValue = isParagraph ? undefined : { value: option.model };
editor.execute( commandName, commandValue );
editor.editing.view.focus();
} );
dropdownMenu.append( menuItem );
const command = isParagraph ? paragraphCommand : headingCommand;
// Make sure the dropdown and its items reflect the state of the
// currently active command.
const onValueChange = isParagraph ? onValueChangeParagraph : onValueChangeHeading;
command.on( 'change:value', onValueChange );
onValueChange();
// Heading commands can become disabled, e.g. when the editor is read-only.
// Make sure the UI reflects this state change.
command.on( 'change:isEnabled', onIsEnabledChange );
onIsEnabledChange();
function onValueChangeHeading() {
const isActive = !isParagraph && command.value === option.model;
if ( isActive ) {
dropdownToggle.children( ':first' ).text( option.title );
}
menuItem.toggleClass( 'active', isActive );
}
function onValueChangeParagraph() {
if ( command.value ) {
dropdownToggle.children( ':first' ).text( option.title );
}
menuItem.toggleClass( 'active', command.value );
}
function onIsEnabledChange() {
dropdownToggle.attr( 'disabled', () => !command.isEnabled );
}
} );
}
}
// Finally, create the BootstrapEditor instance with a selected set of features.
BootstrapEditor
.create( $( '#editor' ).get( 0 ), {
plugins: [
Clipboard, Enter, Typing, Paragraph, EasyImage, Image, ImageUpload, CloudServices,
BoldEditing, ItalicEditing, UnderlineEditing, HeadingEditing, UndoEditing
],
cloudServices: CS_CONFIG
} )
.then( editor => {
window.editor = editor;
const readOnlyLock = Symbol( 'read-only-lock' );
const button = window.document.getElementById( 'toggle-readonly' );
let isReadOnly = false;
button.addEventListener( 'click', () => {
if ( isReadOnly ) {
editor.disableReadOnlyMode( readOnlyLock );
}
else {
editor.enableReadOnlyMode( readOnlyLock );
}
isReadOnly = !isReadOnly;
button.textContent = isReadOnly ?
'Turn off read-only mode' :
'Turn on read-only mode';
editor.editing.view.focus();
} );
} )
.catch( err => {
console.error( err.stack );
} );