inline-widget.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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, window, document */
  6. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  7. import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
  8. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  9. import Heading from '@ckeditor/ckeditor5-heading/src/heading';
  10. import List from '@ckeditor/ckeditor5-list/src/list';
  11. import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
  12. import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
  13. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  14. import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget/src/utils';
  15. import Widget from '@ckeditor/ckeditor5-widget/src/widget';
  16. import Command from '@ckeditor/ckeditor5-core/src/command';
  17. import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
  18. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  19. import Model from '@ckeditor/ckeditor5-ui/src/model';
  20. class PlaceholderCommand extends Command {
  21. execute( { value } ) {
  22. const editor = this.editor;
  23. editor.model.change( writer => {
  24. // Create a <placeholder> elment with the "name" attribute...
  25. const placeholder = writer.createElement( 'placeholder', { name: value } );
  26. // ... and insert it into the document.
  27. editor.model.insertContent( placeholder );
  28. // Put the selection on the inserted element.
  29. writer.setSelection( placeholder, 'on' );
  30. } );
  31. }
  32. refresh() {
  33. const model = this.editor.model;
  34. const selection = model.document.selection;
  35. const isAllowed = model.schema.checkChild( selection.focus.parent, 'placeholder' );
  36. this.isEnabled = isAllowed;
  37. }
  38. }
  39. class Placeholder extends Plugin {
  40. static get requires() {
  41. return [ PlaceholderEditing, PlaceholderUI ];
  42. }
  43. }
  44. class PlaceholderUI extends Plugin {
  45. init() {
  46. const editor = this.editor;
  47. const t = editor.t;
  48. const placeholderNames = editor.config.get( 'placeholderConfig.types' );
  49. // The "placeholder" dropdown must be registered among the UI components of the editor
  50. // to be displayed in the toolbar.
  51. editor.ui.componentFactory.add( 'placeholder', locale => {
  52. const dropdownView = createDropdown( locale );
  53. // Populate the list in the dropdown with items.
  54. addListToDropdown( dropdownView, getDropdownItemsDefinitions( placeholderNames ) );
  55. dropdownView.buttonView.set( {
  56. // The t() function helps localize the editor. All strings enclosed in t() can be
  57. // translated and change when the language of the editor changes.
  58. label: t( 'Placeholder' ),
  59. tooltip: true,
  60. withText: true
  61. } );
  62. // Disable the placeholder button when the command is disabled.
  63. const command = editor.commands.get( 'placeholder' );
  64. dropdownView.bind( 'isEnabled' ).to( command );
  65. // Execute the command when the dropdown item is clicked (executed).
  66. this.listenTo( dropdownView, 'execute', evt => {
  67. editor.execute( 'placeholder', { value: evt.source.commandParam } );
  68. editor.editing.view.focus();
  69. } );
  70. return dropdownView;
  71. } );
  72. }
  73. }
  74. function getDropdownItemsDefinitions( placeholderNames ) {
  75. const itemDefinitions = new Collection();
  76. for ( const name of placeholderNames ) {
  77. const definition = {
  78. type: 'button',
  79. model: new Model( {
  80. commandParam: name,
  81. label: name,
  82. withText: true
  83. } )
  84. };
  85. // Add the item definition to the collection.
  86. itemDefinitions.add( definition );
  87. }
  88. return itemDefinitions;
  89. }
  90. class PlaceholderEditing extends Plugin {
  91. static get requires() {
  92. return [ Widget ];
  93. }
  94. init() {
  95. console.log( 'PlaceholderEditing#init() got called' );
  96. this._defineSchema();
  97. this._defineConverters();
  98. this.editor.commands.add( 'placeholder', new PlaceholderCommand( this.editor ) );
  99. this.editor.editing.mapper.on(
  100. 'viewToModelPosition',
  101. viewToModelPositionOutsideModelElement( this.editor.model, viewElement => viewElement.hasClass( 'placeholder' ) )
  102. );
  103. this.editor.config.define( 'placeholderConfig', {
  104. types: [ 'date', 'first name', 'surname' ]
  105. } );
  106. }
  107. _defineSchema() {
  108. const schema = this.editor.model.schema;
  109. schema.register( 'placeholder', {
  110. // Allow wherever text is allowed:
  111. allowWhere: '$text',
  112. // The placeholder will act as an inline node:
  113. isInline: true,
  114. // The inline widget is self-contained so it cannot be split by the caret and it can be selected:
  115. isObject: true,
  116. // The placeholder can have many types, like date, name, surname, etc:
  117. allowAttributes: [ 'name' ]
  118. } );
  119. }
  120. _defineConverters() {
  121. const conversion = this.editor.conversion;
  122. conversion.for( 'upcast' ).elementToElement( {
  123. view: {
  124. name: 'span',
  125. classes: [ 'placeholder' ]
  126. },
  127. model: ( viewElement, modelWriter ) => {
  128. // Extract the "name" from "{name}".
  129. const name = viewElement.getChild( 0 ).data.slice( 1, -1 );
  130. return modelWriter.createElement( 'placeholder', { name } );
  131. }
  132. } );
  133. conversion.for( 'editingDowncast' ).elementToElement( {
  134. model: 'placeholder',
  135. view: ( modelItem, viewWriter ) => {
  136. const widgetElement = createPlaceholderView( modelItem, viewWriter );
  137. // Enable widget handling on a placeholder element inside the editing view.
  138. return toWidget( widgetElement, viewWriter );
  139. }
  140. } );
  141. conversion.for( 'dataDowncast' ).elementToElement( {
  142. model: 'placeholder',
  143. view: createPlaceholderView
  144. } );
  145. // Helper method for both downcast converters.
  146. function createPlaceholderView( modelItem, viewWriter ) {
  147. const name = modelItem.getAttribute( 'name' );
  148. const placeholderView = viewWriter.createContainerElement( 'span', {
  149. class: 'placeholder'
  150. } );
  151. // Insert the placeholder name (as a text).
  152. const innerText = viewWriter.createText( '{' + name + '}' );
  153. viewWriter.insert( viewWriter.createPositionAt( placeholderView, 0 ), innerText );
  154. return placeholderView;
  155. }
  156. }
  157. }
  158. ClassicEditor
  159. .create( document.querySelector( '#snippet-inline-widget' ), {
  160. plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, Placeholder ],
  161. toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'placeholder' ],
  162. placeholderConfig: {
  163. types: [ 'date', 'color', 'first name', 'surname' ]
  164. }
  165. } )
  166. .then( editor => {
  167. console.log( 'Editor was initialized', editor );
  168. // Expose for playing in the console.
  169. window.editor = editor;
  170. } )
  171. .catch( error => {
  172. console.error( error.stack );
  173. } );