8
0

multi-root-editor.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  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();
  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. view.render();
  160. for ( const editable of this.view.editables ) {
  161. // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
  162. // But it can be available earlier if a DOM element has been passed to DecoupledEditor.create().
  163. const editableElement = editable.element;
  164. // Register the editable UI view in the editor. A single editor instance can aggregate multiple
  165. // editable areas (roots) but the decoupled editor has only one.
  166. this._editableElements.set( editable.name, editableElement );
  167. // Let the global focus tracker know that the editable UI element is focusable and
  168. // belongs to the editor. From now on, the focus tracker will sustain the editor focus
  169. // as long as the editable is focused (e.g. the user is typing).
  170. this.focusTracker.add( editableElement );
  171. // Let the editable UI element respond to the changes in the global editor focus
  172. // tracker. It has been added to the same tracker a few lines above but, in reality, there are
  173. // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
  174. // as they have focus, the editable should act like it is focused too (although technically
  175. // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
  176. // Doing otherwise will result in editable focus styles disappearing, once e.g. the
  177. // toolbar gets focused.
  178. editable.bind( 'isFocused' ).to( this.focusTracker );
  179. // Bind the editable UI element to the editing view, making it an end– and entry–point
  180. // of the editor's engine. This is where the engine meets the UI.
  181. editingView.attachDomRoot( editableElement, editable.name );
  182. }
  183. this._initPlaceholder();
  184. this._initToolbar();
  185. this.fire( 'ready' );
  186. }
  187. /**
  188. * @inheritDoc
  189. */
  190. destroy() {
  191. const view = this.view;
  192. const editingView = this.editor.editing.view;
  193. for ( const editable of this.view.editables ) {
  194. editingView.detachDomRoot( editable.name );
  195. }
  196. view.destroy();
  197. super.destroy();
  198. }
  199. /**
  200. * Initializes the inline editor toolbar and its panel.
  201. *
  202. * @private
  203. */
  204. _initToolbar() {
  205. const editor = this.editor;
  206. const view = this.view;
  207. const toolbar = view.toolbar;
  208. toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
  209. enableToolbarKeyboardFocus( {
  210. origin: editor.editing.view,
  211. originFocusTracker: this.focusTracker,
  212. originKeystrokeHandler: editor.keystrokes,
  213. toolbar
  214. } );
  215. }
  216. /**
  217. * Enable the placeholder text on the editing root, if any was configured.
  218. *
  219. * @private
  220. */
  221. _initPlaceholder() {
  222. const editor = this.editor;
  223. const editingView = editor.editing.view;
  224. for ( const editable of this.view.editables ) {
  225. const editingRoot = editingView.document.getRoot( editable.name );
  226. const sourceElement = this.getEditableElement( editable.name );
  227. const placeholderText = editor.config.get( 'placeholder' ) ||
  228. sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );
  229. if ( placeholderText ) {
  230. enablePlaceholder( {
  231. view: editingView,
  232. element: editingRoot,
  233. text: placeholderText,
  234. isDirectHost: false
  235. } );
  236. }
  237. }
  238. }
  239. }
  240. /**
  241. * The multiroot editor UI view. It is a virtual view providing an inline editable, but without
  242. * any specific arrangement of the components in the DOM.
  243. *
  244. * @extends module:ui/editorui/editoruiview~EditorUIView
  245. */
  246. class MultirootEditorUIView extends EditorUIView {
  247. /**
  248. * Creates an instance of the multiroot editor UI view.
  249. *
  250. * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
  251. * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
  252. * @param {Object.<String,HTMLElement>} editableElements The list of editable elements, containing name and html element
  253. * for each editable.
  254. */
  255. constructor( locale, editingView, editableElements ) {
  256. super( locale );
  257. /**
  258. * The main toolbar of the decoupled editor UI.
  259. *
  260. * @readonly
  261. * @member {module:ui/toolbar/toolbarview~ToolbarView}
  262. */
  263. this.toolbar = new ToolbarView( locale );
  264. /**
  265. * The editables of the multiroot editor UI.
  266. *
  267. * @readonly
  268. * @member {Array.<module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView>}
  269. */
  270. this.editables = [];
  271. // Create InlineEditableUIView instance for each editable.
  272. for ( const editableName of Object.keys( editableElements ) ) {
  273. const editable = new InlineEditableUIView( locale, editingView, editableElements[ editableName ] );
  274. editable.name = editableName;
  275. this.editables.push( editable );
  276. }
  277. // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
  278. // Also because of the above, make sure the toolbar supports rounded corners.
  279. Template.extend( this.toolbar.template, {
  280. attributes: {
  281. class: [
  282. 'ck-reset_all',
  283. 'ck-rounded-corners'
  284. ]
  285. }
  286. } );
  287. }
  288. /**
  289. * @inheritDoc
  290. */
  291. render() {
  292. super.render();
  293. this.registerChild( this.editables );
  294. this.registerChild( [ this.toolbar ] );
  295. }
  296. }
  297. // Initialize editor
  298. MultirootEditor
  299. .create( {
  300. header: document.querySelector( '#header' ),
  301. content: document.querySelector( '#content' ),
  302. footerleft: document.querySelector( '#footer-left' ),
  303. footerright: document.querySelector( '#footer-right' )
  304. }, {
  305. plugins: [ Essentials, Paragraph, Heading, Bold, Italic, List, Link, BlockQuote, Image, ImageCaption,
  306. ImageStyle, ImageToolbar, ImageUpload, Table, TableToolbar, MediaEmbed, EasyImage ],
  307. toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'imageUpload', 'blockQuote',
  308. 'insertTable', 'mediaEmbed', 'undo', 'redo' ],
  309. image: {
  310. toolbar: [ 'imageTextAlternative', '|', 'imageStyle:alignLeft', 'imageStyle:full', 'imageStyle:alignRight' ],
  311. styles: [ 'full', 'alignLeft', 'alignRight' ]
  312. },
  313. table: {
  314. contentToolbar: [
  315. 'tableColumn',
  316. 'tableRow',
  317. 'mergeTableCells'
  318. ]
  319. },
  320. placeholder: 'Type your text here...',
  321. cloudServices: CS_CONFIG
  322. } )
  323. .then( newEditor => {
  324. document.querySelector( '#toolbar' ).appendChild( newEditor.ui.view.toolbar.element );
  325. window.editor = newEditor;
  326. } )
  327. .catch( err => {
  328. console.error( err.stack );
  329. } );