8
0

multi-root-editor.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals console:false, document, window */
  6. // Multiroot editor dependencies.
  7. import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
  8. import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
  9. import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
  10. import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
  11. import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
  12. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  13. import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
  14. import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
  15. import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
  16. import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
  17. import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
  18. import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
  19. import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
  20. import Template from '@ckeditor/ckeditor5-ui/src/template';
  21. // Editor sample dependencies.
  22. import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
  23. import Heading from '@ckeditor/ckeditor5-heading/src/heading';
  24. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  25. import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
  26. import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
  27. import List from '@ckeditor/ckeditor5-list/src/list';
  28. import Link from '@ckeditor/ckeditor5-link/src/link';
  29. import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
  30. import Image from '@ckeditor/ckeditor5-image/src/image';
  31. import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
  32. import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
  33. import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
  34. import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
  35. import Table from '@ckeditor/ckeditor5-table/src/table';
  36. import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
  37. import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
  38. import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
  39. import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
  40. /**
  41. * The multiroot editor implementation. It provides an inline editables and a toolbar.
  42. *
  43. * Unlike other editors, the toolbar is not rendered automatically and needs to be attached to the DOM manually.
  44. *
  45. * This type of an editor is dedicated to integrations which require a customized UI with an open
  46. * structure, allowing developers to specify the exact location of the interface.
  47. *
  48. * @mixes module:core/editor/utils/dataapimixin~DataApiMixin
  49. * @implements module:core/editor/editorwithui~EditorWithUI
  50. * @extends module:core/editor/editor~Editor
  51. */
  52. class MultirootEditor extends Editor {
  53. /**
  54. * Creates an instance of the multiroot editor.
  55. *
  56. * **Note:** Do not use the constructor to create editor instances. Use the static `MultirootEditor.create()` method instead.
  57. *
  58. * @protected
  59. * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
  60. * for the created editor (on which the editor will be initialized).
  61. * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
  62. */
  63. constructor( sourceElements, config ) {
  64. super( config );
  65. this.data.processor = new HtmlDataProcessor( this.data.viewDocument );
  66. // Create root and UIView element for each editable container.
  67. for ( const rootName of Object.keys( sourceElements ) ) {
  68. this.model.document.createRoot( '$root', rootName );
  69. }
  70. this.ui = new MultirootEditorUI( this, new MultirootEditorUIView( this.locale, this.editing.view, sourceElements ) );
  71. }
  72. /**
  73. * @inheritDoc
  74. */
  75. destroy() {
  76. // Cache the data and editable DOM elements, then destroy.
  77. // It's safe to assume that the model->view conversion will not work after super.destroy(),
  78. // same as `ui.getEditableElement()` method will not return editables.
  79. const data = {};
  80. const editables = {};
  81. const editablesNames = Array.from( this.ui.getEditableElementsNames() );
  82. for ( const rootName of editablesNames ) {
  83. data[ rootName ] = this.getData( { rootName } );
  84. editables[ rootName ] = this.ui.getEditableElement( rootName );
  85. }
  86. this.ui.destroy();
  87. return super.destroy()
  88. .then( () => {
  89. for ( const rootName of editablesNames ) {
  90. setDataInElement( editables[ rootName ], data[ rootName ] );
  91. }
  92. } );
  93. }
  94. /**
  95. * Creates a multiroot editor instance.
  96. *
  97. * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
  98. * for the created editor (on which the editor will be initialized).
  99. * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
  100. * @returns {Promise} A promise resolved once the editor is ready. The promise returns the created multiroot editor instance.
  101. */
  102. static create( sourceElements, config ) {
  103. return new Promise( resolve => {
  104. const editor = new this( sourceElements, config );
  105. resolve(
  106. editor.initPlugins()
  107. .then( () => editor.ui.init() )
  108. .then( () => {
  109. const initialData = {};
  110. // Create initial data object containing data from all roots.
  111. for ( const rootName of Object.keys( sourceElements ) ) {
  112. initialData[ rootName ] = getDataFromElement( sourceElements[ rootName ] );
  113. }
  114. return editor.data.init( initialData );
  115. } )
  116. .then( () => editor.fire( 'ready' ) )
  117. .then( () => editor )
  118. );
  119. } );
  120. }
  121. }
  122. mix( MultirootEditor, DataApiMixin );
  123. /**
  124. * The multiroot editor UI class.
  125. *
  126. * @extends module:core/editor/editorui~EditorUI
  127. */
  128. class MultirootEditorUI extends EditorUI {
  129. /**
  130. * Creates an instance of the multiroot editor UI class.
  131. *
  132. * @param {module:core/editor/editor~Editor} editor The editor instance.
  133. * @param {module:ui/editorui/editoruiview~EditorUIView} view The view of the UI.
  134. */
  135. constructor( editor, view ) {
  136. super( editor );
  137. /**
  138. * The main (top–most) view of the editor UI.
  139. *
  140. * @readonly
  141. * @member {module:ui/editorui/editoruiview~EditorUIView} #view
  142. */
  143. this.view = view;
  144. /**
  145. * A normalized `config.toolbar` object.
  146. *
  147. * @type {Object}
  148. * @private
  149. */
  150. this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) );
  151. }
  152. /**
  153. * Initializes the UI.
  154. */
  155. init() {
  156. const view = this.view;
  157. const editor = this.editor;
  158. const editingView = editor.editing.view;
  159. let lastFocusedEditableElement;
  160. view.render();
  161. // Keep track of the last focused editable element. Knowing which one was focused
  162. // is useful when the focus moves from editable to other UI components like balloons
  163. // (especially inputs) but the editable remains the "focus context" (e.g. link balloon
  164. // attached to a link in an editable). In this case, the editable should preserve visual
  165. // focus styles.
  166. this.focusTracker.on( 'change:focusedElement', ( evt, name, focusedElement ) => {
  167. for ( const editable of this.view.editables ) {
  168. if ( editable.element === focusedElement ) {
  169. lastFocusedEditableElement = editable.element;
  170. }
  171. }
  172. } );
  173. // If the focus tracker loses focus, stop tracking the last focused editable element.
  174. // Wherever the focus is restored, it will no longer be in the context of that editable
  175. // because the focus "came from the outside", as opposed to the focus moving from one element
  176. // to another withing the editor UI.
  177. this.focusTracker.on( 'change:isFocused', ( evt, name, isFocused ) => {
  178. if ( !isFocused ) {
  179. lastFocusedEditableElement = null;
  180. }
  181. } );
  182. for ( const editable of this.view.editables ) {
  183. // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
  184. // But it can be available earlier if a DOM element has been passed to DecoupledEditor.create().
  185. const editableElement = editable.element;
  186. // Register the editable UI view in the editor. A single editor instance can aggregate multiple
  187. // editable areas (roots) but the decoupled editor has only one.
  188. this.setEditableElement( editable.name, editableElement );
  189. // Let the global focus tracker know that the editable UI element is focusable and
  190. // belongs to the editor. From now on, the focus tracker will sustain the editor focus
  191. // as long as the editable is focused (e.g. the user is typing).
  192. this.focusTracker.add( editableElement );
  193. // Let the editable UI element respond to the changes in the global editor focus
  194. // tracker. It has been added to the same tracker a few lines above but, in reality, there are
  195. // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
  196. // as they have focus, the editable should act like it is focused too (although technically
  197. // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
  198. // Doing otherwise will result in editable focus styles disappearing, once e.g. the
  199. // toolbar gets focused.
  200. editable.bind( 'isFocused' ).to( this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement',
  201. ( isFocused, focusedElement ) => {
  202. // When the focus tracker is blurred, it means the focus moved out of the editor UI.
  203. // No editable will maintain focus then.
  204. if ( !isFocused ) {
  205. return false;
  206. }
  207. // If the focus tracker says the editor UI is focused and currently focused element
  208. // is the editable, then the editable should be visually marked as focused too.
  209. if ( focusedElement === editableElement ) {
  210. return true;
  211. }
  212. // If the focus tracker says the editor UI is focused but the focused element is
  213. // not an editable, it is possible that the editable is still (context–)focused.
  214. // For instance, the focused element could be an input inside of a balloon attached
  215. // to the content in the editable. In such case, the editable should remain _visually_
  216. // focused even though technically the focus is somewhere else. The focus moved from
  217. // the editable to the input but the focus context remained the same.
  218. else {
  219. return lastFocusedEditableElement === editableElement;
  220. }
  221. } );
  222. // Bind the editable UI element to the editing view, making it an end– and entry–point
  223. // of the editor's engine. This is where the engine meets the UI.
  224. editingView.attachDomRoot( editableElement, editable.name );
  225. }
  226. this._initPlaceholder();
  227. this._initToolbar();
  228. this.fire( 'ready' );
  229. }
  230. /**
  231. * @inheritDoc
  232. */
  233. destroy() {
  234. const view = this.view;
  235. const editingView = this.editor.editing.view;
  236. for ( const editable of this.view.editables ) {
  237. editingView.detachDomRoot( editable.name );
  238. }
  239. view.destroy();
  240. super.destroy();
  241. }
  242. /**
  243. * Initializes the inline editor toolbar and its panel.
  244. *
  245. * @private
  246. */
  247. _initToolbar() {
  248. const editor = this.editor;
  249. const view = this.view;
  250. const toolbar = view.toolbar;
  251. toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
  252. enableToolbarKeyboardFocus( {
  253. origin: editor.editing.view,
  254. originFocusTracker: this.focusTracker,
  255. originKeystrokeHandler: editor.keystrokes,
  256. toolbar
  257. } );
  258. }
  259. /**
  260. * Enable the placeholder text on the editing root, if any was configured.
  261. *
  262. * @private
  263. */
  264. _initPlaceholder() {
  265. const editor = this.editor;
  266. const editingView = editor.editing.view;
  267. for ( const editable of this.view.editables ) {
  268. const editingRoot = editingView.document.getRoot( editable.name );
  269. const sourceElement = this.getEditableElement( editable.name );
  270. const placeholderText = editor.config.get( 'placeholder' )[ editable.name ] ||
  271. sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );
  272. if ( placeholderText ) {
  273. enablePlaceholder( {
  274. view: editingView,
  275. element: editingRoot,
  276. text: placeholderText,
  277. isDirectHost: false
  278. } );
  279. }
  280. }
  281. }
  282. }
  283. /**
  284. * The multiroot editor UI view. It is a virtual view providing an inline editable, but without
  285. * any specific arrangement of the components in the DOM.
  286. *
  287. * @extends module:ui/editorui/editoruiview~EditorUIView
  288. */
  289. class MultirootEditorUIView extends EditorUIView {
  290. /**
  291. * Creates an instance of the multiroot editor UI view.
  292. *
  293. * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
  294. * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
  295. * @param {Object.<String,HTMLElement>} editableElements The list of editable elements, containing name and html element
  296. * for each editable.
  297. */
  298. constructor( locale, editingView, editableElements ) {
  299. super( locale );
  300. /**
  301. * The main toolbar of the decoupled editor UI.
  302. *
  303. * @readonly
  304. * @member {module:ui/toolbar/toolbarview~ToolbarView}
  305. */
  306. this.toolbar = new ToolbarView( locale );
  307. /**
  308. * The editables of the multiroot editor UI.
  309. *
  310. * @readonly
  311. * @member {Array.<module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView>}
  312. */
  313. this.editables = [];
  314. // Create InlineEditableUIView instance for each editable.
  315. for ( const editableName of Object.keys( editableElements ) ) {
  316. const editable = new InlineEditableUIView( locale, editingView, editableElements[ editableName ] );
  317. editable.name = editableName;
  318. this.editables.push( editable );
  319. }
  320. // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
  321. // Because of the above, make sure the toolbar supports rounded corners.
  322. // Also, make sure the toolbar has the proper dir attribute because its ancestor may not have one
  323. // and some toolbar item styles depend on this attribute.
  324. Template.extend( this.toolbar.template, {
  325. attributes: {
  326. class: [
  327. 'ck-reset_all',
  328. 'ck-rounded-corners'
  329. ],
  330. dir: locale.uiLanguageDirection
  331. }
  332. } );
  333. }
  334. /**
  335. * @inheritDoc
  336. */
  337. render() {
  338. super.render();
  339. this.registerChild( this.editables );
  340. this.registerChild( [ this.toolbar ] );
  341. }
  342. }
  343. // Initialize editor
  344. MultirootEditor
  345. .create( {
  346. header: document.querySelector( '#header' ),
  347. content: document.querySelector( '#content' ),
  348. footerleft: document.querySelector( '#footer-left' ),
  349. footerright: document.querySelector( '#footer-right' )
  350. }, {
  351. plugins: [ Essentials, Paragraph, Heading, Bold, Italic, List, Link, BlockQuote, Image, ImageCaption,
  352. ImageStyle, ImageToolbar, ImageUpload, Table, TableToolbar, MediaEmbed, EasyImage ],
  353. toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'imageUpload', 'blockQuote',
  354. 'insertTable', 'mediaEmbed', 'undo', 'redo' ],
  355. image: {
  356. toolbar: [ 'imageTextAlternative', '|', 'imageStyle:full',
  357. 'imageStyle:side' ],
  358. styles: [ 'full', 'side' ]
  359. },
  360. table: {
  361. contentToolbar: [
  362. 'tableColumn',
  363. 'tableRow',
  364. 'mergeTableCells'
  365. ]
  366. },
  367. placeholder: {
  368. header: 'Header text goes here',
  369. content: 'Type content here',
  370. footerleft: 'Left footer content',
  371. footerright: 'Right footer content'
  372. },
  373. cloudServices: CS_CONFIG
  374. } )
  375. .then( newEditor => {
  376. document.querySelector( '#toolbar' ).appendChild( newEditor.ui.view.toolbar.element );
  377. window.editor = newEditor;
  378. } )
  379. .catch( err => {
  380. console.error( err.stack );
  381. } );