8
0

bootstrap-ui-inner.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 $, window, console:false */
  6. // Basic classes to create an editor.
  7. import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
  8. import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
  9. import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
  10. import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
  11. import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
  12. import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
  13. // Interfaces to extend basic Editor API.
  14. import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
  15. import ElementApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin';
  16. // Helper function for adding interfaces to the Editor class.
  17. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  18. // Helper function that gets data from HTML element that the Editor is attached to.
  19. import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
  20. // Helper function that binds editor with HTMLForm element.
  21. import attachToForm from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform';
  22. // Basic features that every editor should enable.
  23. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  24. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  25. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  26. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  27. import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
  28. // Basic features to associated with the edited content.
  29. import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
  30. import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
  31. import UnderlineEditing from '@ckeditor/ckeditor5-basic-styles/src/underline/underlineediting';
  32. import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
  33. // The easy image integration.
  34. import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
  35. import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
  36. // Extending the Editor class, which brings base editor API.
  37. export default class BootstrapEditor extends Editor {
  38. constructor( element, config ) {
  39. super( config );
  40. // Remember the element the editor is created with.
  41. this.sourceElement = element;
  42. // Use the HTML data processor in this editor.
  43. this.data.processor = new HtmlDataProcessor( this.data.viewDocument );
  44. // Create the ("main") root element of the model tree.
  45. this.model.document.createRoot();
  46. // The UI layer of the editor.
  47. this.ui = new BootstrapEditorUI( this );
  48. // When editor#element is a textarea inside a form element
  49. // then content of this textarea will be updated on form submit.
  50. attachToForm( this );
  51. }
  52. destroy() {
  53. // When destroyed, editor sets the output of editor#getData() into editor#element...
  54. this.updateSourceElement();
  55. // ...and destroys the UI.
  56. this.ui.destroy();
  57. return super.destroy();
  58. }
  59. static create( element, config ) {
  60. return new Promise( resolve => {
  61. const editor = new this( element, config );
  62. resolve(
  63. editor.initPlugins()
  64. // Initialize the UI first. See the BootstrapEditorUI class to learn more.
  65. .then( () => editor.ui.init( element ) )
  66. // Fill the editable with the initial data.
  67. .then( () => editor.data.init( getDataFromElement( element ) ) )
  68. // Fire the `editor#ready` event that announce the editor is complete and ready to use.
  69. .then( () => editor.fire( 'ready' ) )
  70. .then( () => editor )
  71. );
  72. } );
  73. }
  74. }
  75. // Mixing interfaces, which extends basic editor API.
  76. mix( BootstrapEditor, DataApiMixin );
  77. mix( BootstrapEditor, ElementApiMixin );
  78. // The class organizing the UI of the editor, binding it with existing Bootstrap elements in DOM.
  79. class BootstrapEditorUI extends EditorUI {
  80. constructor( editor ) {
  81. super( editor );
  82. // A helper to easily replace the editor#element with editor.editable#element.
  83. this._elementReplacer = new ElementReplacer();
  84. // The global UI view of the editor. It aggregates various Bootstrap DOM elements.
  85. const view = this._view = new EditorUIView( editor.locale );
  86. // This is the main editor element in the DOM.
  87. view.element = $( '.ck-editor' );
  88. // This is the editable view in the DOM. It will replace the data container in the DOM.
  89. view.editable = new InlineEditableUIView( editor.locale, editor.editing.view );
  90. // References to the dropdown elements for further usage. See #_setupBootstrapHeadingDropdown.
  91. view.dropdownMenu = view.element.find( '.dropdown-menu' );
  92. view.dropdownToggle = view.element.find( '.dropdown-toggle' );
  93. // References to the toolbar buttons for further usage. See #_setupBootstrapToolbarButtons.
  94. view.toolbarButtons = {};
  95. [ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( name => {
  96. // Retrieve the jQuery object corresponding with the button in the DOM.
  97. view.toolbarButtons[ name ] = view.element.find( `#${ name }` );
  98. } );
  99. }
  100. // All EditorUI subclasses should expose their view instance
  101. // so other UI classes can access it if necessary.
  102. get view() {
  103. return this._view;
  104. }
  105. init( replacementElement ) {
  106. const editor = this.editor;
  107. const view = this.view;
  108. const editingView = editor.editing.view;
  109. // Make sure the EditorUIView is rendered. This will, for instance, create a place for UI elements
  110. // like floating panels detached from the main editor UI in DOM.
  111. this._view.render();
  112. // Create an editing root in the editing layer. It will correspond with the
  113. // document root created in the constructor().
  114. const editingRoot = editingView.document.getRoot();
  115. // The editable UI and editing root should share the same name.
  116. view.editable.name = editingRoot.rootName;
  117. // Render the editable component in the DOM first.
  118. view.editable.render();
  119. const editableElement = view.editable.element;
  120. // Register editable element so it is available via getEditableElement() method.
  121. this.setEditableElement( view.editable.name, editableElement );
  122. // Let the editable UI element respond to the changes in the global editor focus tracker
  123. // and let the focus tracker know about the editable element.
  124. this.focusTracker.add( editableElement );
  125. view.editable.bind( 'isFocused' ).to( this.focusTracker );
  126. // Bind the editable UI element to the editing view, making it an end– and entry–point
  127. // of the editor's engine. This is where the engine meets the UI.
  128. editingView.attachDomRoot( editableElement );
  129. // Setup the existing, external Bootstrap UI so it works with the rest of the editor.
  130. this._setupBootstrapToolbarButtons();
  131. this._setupBootstrapHeadingDropdown();
  132. // Replace the editor#element with editor.editable#element.
  133. this._elementReplacer.replace( replacementElement, editableElement );
  134. // Tell the world that the UI of the editor is ready to use.
  135. this.fire( 'ready' );
  136. }
  137. destroy() {
  138. // Restore the original editor#element.
  139. this._elementReplacer.restore();
  140. // Destroy the view.
  141. this._view.editable.destroy();
  142. this._view.destroy();
  143. super.destroy();
  144. }
  145. // This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
  146. _setupBootstrapToolbarButtons() {
  147. const editor = this.editor;
  148. for ( const name in this.view.toolbarButtons ) {
  149. // Retrieve the editor command corresponding with the id of the button in DOM.
  150. const command = editor.commands.get( name );
  151. const button = this.view.toolbarButtons[ name ];
  152. // Clicking on the buttons should execute the editor command...
  153. button.click( () => editor.execute( name ) );
  154. // ...but it should not steal the focus so the editing is uninterrupted.
  155. button.mousedown( evt => evt.preventDefault() );
  156. const onValueChange = () => {
  157. button.toggleClass( 'active', command.value );
  158. };
  159. const onIsEnabledChange = () => {
  160. button.attr( 'disabled', () => !command.isEnabled );
  161. };
  162. // Commands can become disabled, e.g. when the editor is read-only.
  163. // Make sure the buttons reflect this state change.
  164. command.on( 'change:isEnabled', onIsEnabledChange );
  165. onIsEnabledChange();
  166. // Bold, Italic and Underline commands have a value that changes
  167. // when the selection starts in an element the command creates.
  168. // The button should indicate that e.g. editing text which is already bold.
  169. if ( !new Set( [ 'undo', 'redo' ] ).has( name ) ) {
  170. command.on( 'change:value', onValueChange );
  171. onValueChange();
  172. }
  173. }
  174. }
  175. // This method activates the headings dropdown in the toolbar.
  176. _setupBootstrapHeadingDropdown() {
  177. const editor = this.editor;
  178. const dropdownMenu = this.view.dropdownMenu;
  179. const dropdownToggle = this.view.dropdownToggle;
  180. // Retrieve the editor commands for heading and paragraph.
  181. const headingCommand = editor.commands.get( 'heading' );
  182. const paragraphCommand = editor.commands.get( 'paragraph' );
  183. // Create a dropdown menu entry for each heading configuration option.
  184. editor.config.get( 'heading.options' ).map( option => {
  185. // Check is options is paragraph or heading as their commands slightly differ.
  186. const isParagraph = option.model === 'paragraph';
  187. // Create the menu item DOM element.
  188. const menuItem = $(
  189. `<a href="#" class="dropdown-item heading-item_${ option.model }">` +
  190. `${ option.title }` +
  191. '</a>'
  192. );
  193. // Upon click, the dropdown menu item should execute the command and focus
  194. // the editing view to keep the editing process uninterrupted.
  195. menuItem.click( () => {
  196. const commandName = isParagraph ? 'paragraph' : 'heading';
  197. const commandValue = isParagraph ? undefined : { value: option.model };
  198. editor.execute( commandName, commandValue );
  199. editor.editing.view.focus();
  200. } );
  201. dropdownMenu.append( menuItem );
  202. const command = isParagraph ? paragraphCommand : headingCommand;
  203. // Make sure the dropdown and its items reflect the state of the
  204. // currently active command.
  205. const onValueChange = isParagraph ? onValueChangeParagraph : onValueChangeHeading;
  206. command.on( 'change:value', onValueChange );
  207. onValueChange();
  208. // Heading commands can become disabled, e.g. when the editor is read-only.
  209. // Make sure the UI reflects this state change.
  210. command.on( 'change:isEnabled', onIsEnabledChange );
  211. onIsEnabledChange();
  212. function onValueChangeHeading() {
  213. const isActive = !isParagraph && command.value === option.model;
  214. if ( isActive ) {
  215. dropdownToggle.children( ':first' ).text( option.title );
  216. }
  217. menuItem.toggleClass( 'active', isActive );
  218. }
  219. function onValueChangeParagraph() {
  220. if ( command.value ) {
  221. dropdownToggle.children( ':first' ).text( option.title );
  222. }
  223. menuItem.toggleClass( 'active', command.value );
  224. }
  225. function onIsEnabledChange() {
  226. dropdownToggle.attr( 'disabled', () => !command.isEnabled );
  227. }
  228. } );
  229. }
  230. }
  231. // Finally, create the BootstrapEditor instance with a selected set of features.
  232. BootstrapEditor
  233. .create( $( '#editor' ).get( 0 ), {
  234. plugins: [
  235. Clipboard, Enter, Typing, Paragraph, EasyImage,
  236. BoldEditing, ItalicEditing, UnderlineEditing, HeadingEditing, UndoEditing
  237. ],
  238. cloudServices: CS_CONFIG
  239. } )
  240. .then( editor => {
  241. window.editor = editor;
  242. $( '#toggle-readonly' ).on( 'click', () => {
  243. editor.isReadOnly = !editor.isReadOnly;
  244. } );
  245. } )
  246. .catch( err => {
  247. console.error( err.stack );
  248. } );