| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /* globals console:false, document, window */
- // Multiroot editor dependencies.
- import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
- import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
- import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
- import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
- import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
- import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
- import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
- import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
- import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
- import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
- import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
- import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
- import Template from '@ckeditor/ckeditor5-ui/src/template';
- // Editor sample dependencies.
- import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
- import Heading from '@ckeditor/ckeditor5-heading/src/heading';
- import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
- import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
- import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
- import List from '@ckeditor/ckeditor5-list/src/list';
- import Link from '@ckeditor/ckeditor5-link/src/link';
- import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
- import Image from '@ckeditor/ckeditor5-image/src/image';
- import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
- import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
- import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
- import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
- import Table from '@ckeditor/ckeditor5-table/src/table';
- import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
- import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
- import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
- import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
- /**
- * The multiroot editor implementation. It provides an inline editables and a toolbar.
- *
- * Unlike other editors, the toolbar is not rendered automatically and needs to be attached to the DOM manually.
- *
- * This type of an editor is dedicated to integrations which require a customized UI with an open
- * structure, allowing developers to specify the exact location of the interface.
- *
- * @mixes module:core/editor/utils/dataapimixin~DataApiMixin
- * @implements module:core/editor/editorwithui~EditorWithUI
- * @extends module:core/editor/editor~Editor
- */
- class MultirootEditor extends Editor {
- /**
- * Creates an instance of the multiroot editor.
- *
- * **Note:** Do not use the constructor to create editor instances. Use the static `MultirootEditor.create()` method instead.
- *
- * @protected
- * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
- * for the created editor (on which the editor will be initialized).
- * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
- */
- constructor( sourceElements, config ) {
- super( config );
- this.data.processor = new HtmlDataProcessor( this.data.viewDocument );
- // Create root and UIView element for each editable container.
- for ( const rootName of Object.keys( sourceElements ) ) {
- this.model.document.createRoot( '$root', rootName );
- }
- this.ui = new MultirootEditorUI( this, new MultirootEditorUIView( this.locale, this.editing.view, sourceElements ) );
- }
- /**
- * @inheritDoc
- */
- destroy() {
- // Cache the data and editable DOM elements, then destroy.
- // It's safe to assume that the model->view conversion will not work after super.destroy(),
- // same as `ui.getEditableElement()` method will not return editables.
- const data = {};
- const editables = {};
- const editablesNames = Array.from( this.ui.getEditableElementsNames() );
- for ( const rootName of editablesNames ) {
- data[ rootName ] = this.getData( { rootName } );
- editables[ rootName ] = this.ui.getEditableElement( rootName );
- }
- this.ui.destroy();
- return super.destroy()
- .then( () => {
- for ( const rootName of editablesNames ) {
- setDataInElement( editables[ rootName ], data[ rootName ] );
- }
- } );
- }
- /**
- * Creates a multiroot editor instance.
- *
- * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
- * for the created editor (on which the editor will be initialized).
- * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
- * @returns {Promise} A promise resolved once the editor is ready. The promise returns the created multiroot editor instance.
- */
- static create( sourceElements, config ) {
- return new Promise( resolve => {
- const editor = new this( sourceElements, config );
- resolve(
- editor.initPlugins()
- .then( () => editor.ui.init() )
- .then( () => {
- const initialData = {};
- // Create initial data object containing data from all roots.
- for ( const rootName of Object.keys( sourceElements ) ) {
- initialData[ rootName ] = getDataFromElement( sourceElements[ rootName ] );
- }
- return editor.data.init( initialData );
- } )
- .then( () => editor.fire( 'ready' ) )
- .then( () => editor )
- );
- } );
- }
- }
- mix( MultirootEditor, DataApiMixin );
- /**
- * The multiroot editor UI class.
- *
- * @extends module:core/editor/editorui~EditorUI
- */
- class MultirootEditorUI extends EditorUI {
- /**
- * Creates an instance of the multiroot editor UI class.
- *
- * @param {module:core/editor/editor~Editor} editor The editor instance.
- * @param {module:ui/editorui/editoruiview~EditorUIView} view The view of the UI.
- */
- constructor( editor, view ) {
- super( editor );
- /**
- * The main (top–most) view of the editor UI.
- *
- * @readonly
- * @member {module:ui/editorui/editoruiview~EditorUIView} #view
- */
- this.view = view;
- /**
- * A normalized `config.toolbar` object.
- *
- * @type {Object}
- * @private
- */
- this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) );
- }
- /**
- * Initializes the UI.
- */
- init() {
- const view = this.view;
- const editor = this.editor;
- const editingView = editor.editing.view;
- let lastFocusedEditableElement;
- 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 this.view.editables ) {
- if ( editable.element === focusedElement ) {
- 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 withing the editor UI.
- this.focusTracker.on( 'change:isFocused', ( evt, name, isFocused ) => {
- if ( !isFocused ) {
- lastFocusedEditableElement = null;
- }
- } );
- for ( const editable of this.view.editables ) {
- // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
- // But it can be available earlier if a DOM element has been passed to DecoupledEditor.create().
- const editableElement = editable.element;
- // Register the editable UI view in the editor. A single editor instance can aggregate multiple
- // editable areas (roots) but the decoupled editor has only one.
- this.setEditableElement( editable.name, editableElement );
- // Let the global focus tracker know that the editable UI element is focusable and
- // belongs to the editor. From now on, the focus tracker will sustain the editor focus
- // as long as the editable is focused (e.g. the user is typing).
- this.focusTracker.add( editableElement );
- // Let the editable UI element respond to the changes in the global editor focus
- // tracker. It has been added to the same tracker a few lines above but, in reality, there are
- // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
- // as they have focus, the editable should act like it is focused too (although technically
- // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
- // Doing otherwise will result in editable focus styles disappearing, once e.g. the
- // toolbar gets focused.
- editable.bind( 'isFocused' ).to( this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement',
- ( isFocused, focusedElement ) => {
- // When the focus tracker is blurred, it means the focus moved out of the editor UI.
- // No editable will maintain focus then.
- if ( !isFocused ) {
- return false;
- }
- // If the focus tracker says the editor UI is focused and currently focused element
- // is the editable, then the editable should be visually marked as focused too.
- if ( focusedElement === editableElement ) {
- return true;
- }
- // If the focus tracker says the editor UI is focused but the focused element is
- // not an editable, it is possible that the editable is still (context–)focused.
- // For instance, the focused element could be an input inside of a balloon attached
- // to the content in the editable. In such case, the editable should remain _visually_
- // focused even though technically the focus is somewhere else. The focus moved from
- // the editable to the input but the focus context remained the same.
- else {
- return lastFocusedEditableElement === editableElement;
- }
- } );
- // Bind the editable UI element to the editing view, making it an end– and entry–point
- // of the editor's engine. This is where the engine meets the UI.
- editingView.attachDomRoot( editableElement, editable.name );
- }
- this._initPlaceholder();
- this._initToolbar();
- this.fire( 'ready' );
- }
- /**
- * @inheritDoc
- */
- destroy() {
- const view = this.view;
- const editingView = this.editor.editing.view;
- for ( const editable of this.view.editables ) {
- editingView.detachDomRoot( editable.name );
- }
- view.destroy();
- super.destroy();
- }
- /**
- * Initializes the inline editor toolbar and its panel.
- *
- * @private
- */
- _initToolbar() {
- const editor = this.editor;
- const view = this.view;
- const toolbar = view.toolbar;
- toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
- enableToolbarKeyboardFocus( {
- origin: editor.editing.view,
- originFocusTracker: this.focusTracker,
- originKeystrokeHandler: editor.keystrokes,
- toolbar
- } );
- }
- /**
- * Enable the placeholder text on the editing root, if any was configured.
- *
- * @private
- */
- _initPlaceholder() {
- const editor = this.editor;
- const editingView = editor.editing.view;
- for ( const editable of this.view.editables ) {
- const editingRoot = editingView.document.getRoot( editable.name );
- const sourceElement = this.getEditableElement( editable.name );
- const placeholderText = editor.config.get( 'placeholder' )[ editable.name ] ||
- sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );
- if ( placeholderText ) {
- enablePlaceholder( {
- view: editingView,
- element: editingRoot,
- text: placeholderText,
- isDirectHost: false
- } );
- }
- }
- }
- }
- /**
- * The multiroot editor UI view. It is a virtual view providing an inline editable, but without
- * any specific arrangement of the components in the DOM.
- *
- * @extends module:ui/editorui/editoruiview~EditorUIView
- */
- class MultirootEditorUIView extends EditorUIView {
- /**
- * Creates an instance of the multiroot editor UI view.
- *
- * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
- * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
- * @param {Object.<String,HTMLElement>} editableElements The list of editable elements, containing name and html element
- * for each editable.
- */
- constructor( locale, editingView, editableElements ) {
- super( locale );
- /**
- * The main toolbar of the decoupled editor UI.
- *
- * @readonly
- * @member {module:ui/toolbar/toolbarview~ToolbarView}
- */
- this.toolbar = new ToolbarView( locale );
- /**
- * The editables of the multiroot editor UI.
- *
- * @readonly
- * @member {Array.<module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView>}
- */
- this.editables = [];
- // Create InlineEditableUIView instance for each editable.
- for ( const editableName of Object.keys( editableElements ) ) {
- const editable = new InlineEditableUIView( locale, editingView, editableElements[ editableName ] );
- editable.name = editableName;
- this.editables.push( editable );
- }
- // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
- // Because of the above, make sure the toolbar supports rounded corners.
- // Also, make sure the toolbar has the proper dir attribute because its ancestor may not have one
- // and some toolbar item styles depend on this attribute.
- Template.extend( this.toolbar.template, {
- attributes: {
- class: [
- 'ck-reset_all',
- 'ck-rounded-corners'
- ],
- dir: locale.uiLanguageDirection
- }
- } );
- }
- /**
- * @inheritDoc
- */
- render() {
- super.render();
- this.registerChild( this.editables );
- this.registerChild( [ this.toolbar ] );
- }
- }
- // Initialize editor
- MultirootEditor
- .create( {
- header: document.querySelector( '#header' ),
- content: document.querySelector( '#content' ),
- footerleft: document.querySelector( '#footer-left' ),
- footerright: document.querySelector( '#footer-right' )
- }, {
- plugins: [ Essentials, Paragraph, Heading, Bold, Italic, List, Link, BlockQuote, Image, ImageCaption,
- ImageStyle, ImageToolbar, ImageUpload, Table, TableToolbar, MediaEmbed, EasyImage ],
- toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'imageUpload', 'blockQuote',
- 'insertTable', 'mediaEmbed', 'undo', 'redo' ],
- image: {
- toolbar: [ 'imageTextAlternative', '|', 'imageStyle:full',
- 'imageStyle:side' ],
- styles: [ 'full', 'side' ]
- },
- table: {
- contentToolbar: [
- 'tableColumn',
- 'tableRow',
- 'mergeTableCells'
- ]
- },
- placeholder: {
- header: 'Header text goes here',
- content: 'Type content here',
- footerleft: 'Left footer content',
- footerright: 'Right footer content'
- },
- cloudServices: CS_CONFIG
- } )
- .then( newEditor => {
- document.querySelector( '#toolbar' ).appendChild( newEditor.ui.view.toolbar.element );
- window.editor = newEditor;
- } )
- .catch( err => {
- console.error( err.stack );
- } );
|