8
0

element-reconversion-demo.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals ClassicEditor, toWidget, toWidgetEditable, console, window, prompt, document */
  6. import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
  7. import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
  8. const getTypeFromViewElement = viewElement => {
  9. for ( const type of [ 'info', 'warning' ] ) {
  10. if ( viewElement.hasClass( `info-box-${ type }` ) ) {
  11. return type;
  12. }
  13. }
  14. return 'info';
  15. };
  16. const upcastInfoBox = ( viewElement, { writer } ) => {
  17. const complexInfoBox = writer.createElement( 'complexInfoBox' );
  18. const type = getTypeFromViewElement( viewElement );
  19. writer.setAttribute( 'infoBoxType', type, complexInfoBox );
  20. const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
  21. return child.is( 'element', 'div' ) && child.hasClass( 'info-box-url' );
  22. } );
  23. if ( urlWrapper ) {
  24. writer.setAttribute( 'infoBoxURL', urlWrapper.getChild( 0 ).data, complexInfoBox );
  25. }
  26. return complexInfoBox;
  27. };
  28. function addActionButton( text, callback, domElement, editor ) {
  29. const domDocument = domElement.ownerDocument;
  30. const button = createElement( domDocument, 'button', {}, [ text ] );
  31. button.addEventListener( 'click', () => {
  32. editor.model.change( callback );
  33. } );
  34. domElement.appendChild( button );
  35. return button;
  36. }
  37. const renderActionsView = ( editor, modelElement ) => function( domElement ) {
  38. addActionButton( 'Set URL', writer => {
  39. // eslint-disable-next-line no-alert
  40. const newURL = prompt( 'Set URL', modelElement.getAttribute( 'infoBoxURL' ) || '' );
  41. writer.setAttribute( 'infoBoxURL', newURL, modelElement );
  42. }, domElement, editor );
  43. const currentType = modelElement.getAttribute( 'infoBoxType' );
  44. const newType = currentType === 'info' ? 'warning' : 'info';
  45. addActionButton( `Change to ${ newType }`, writer => {
  46. writer.setAttribute( 'infoBoxType', newType, modelElement );
  47. }, domElement, editor );
  48. const childCount = modelElement.childCount;
  49. const addButton = addActionButton( 'Add content box', writer => {
  50. writer.insertElement( 'complexInfoBoxContent', modelElement, 'end' );
  51. }, domElement, editor );
  52. if ( childCount > 4 ) {
  53. addButton.setAttribute( 'disabled', 'disabled' );
  54. }
  55. const removeButton = addActionButton( 'Remove content box', writer => {
  56. writer.remove( modelElement.getChild( childCount - 1 ) );
  57. }, domElement, editor );
  58. if ( childCount < 3 ) {
  59. removeButton.setAttribute( 'disabled', 'disabled' );
  60. }
  61. };
  62. const downcastInfoBox = editor => {
  63. return ( modelElement, { writer, consumable, mapper } ) => {
  64. const complexInfoBoxView = writer.createContainerElement( 'div', {
  65. class: 'info-box'
  66. } );
  67. const type = modelElement.getAttribute( 'infoBoxType' ) || 'info';
  68. writer.addClass( `info-box-${ type }`, complexInfoBoxView );
  69. for ( const child of modelElement.getChildren() ) {
  70. const childView = writer.createEditableElement( 'div' );
  71. if ( child.is( 'element', 'complexInfoBoxTitle' ) ) {
  72. writer.addClass( 'info-box-title', childView );
  73. } else {
  74. writer.addClass( 'info-box-content', childView );
  75. }
  76. consumable.consume( child, 'insert' );
  77. mapper.bindElements( child, childView );
  78. toWidgetEditable( childView, writer );
  79. writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), childView );
  80. }
  81. const urlAttribute = modelElement.getAttribute( 'infoBoxURL' );
  82. // Do not render empty URL field
  83. if ( urlAttribute ) {
  84. const urlBox = writer.createRawElement( 'div', {
  85. class: 'info-box-url'
  86. }, function( domElement ) {
  87. domElement.innerText = `URL: "${ urlAttribute }"`;
  88. } );
  89. writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), urlBox );
  90. }
  91. const actionsView = writer.createRawElement( 'div', {
  92. class: 'info-box-actions',
  93. contenteditable: 'false', // Prevent editing of the element:
  94. 'data-cke-ignore-events': 'true' // Allows using custom UI elements inside editing view.
  95. }, renderActionsView( editor, modelElement ) );
  96. writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), actionsView );
  97. return toWidget( complexInfoBoxView, writer, {
  98. widgetLabel: 'Complex Info Box'
  99. } );
  100. };
  101. };
  102. class ComplexInfoBox {
  103. constructor( editor ) {
  104. this._defineSchema( editor );
  105. this._defineConversion( editor );
  106. }
  107. _defineConversion( editor ) {
  108. const conversion = editor.conversion;
  109. conversion.for( 'upcast' )
  110. .elementToElement( {
  111. view: { name: 'div', classes: [ 'info-box' ] },
  112. model: upcastInfoBox
  113. } )
  114. .elementToElement( {
  115. view: { name: 'div', classes: [ 'info-box-title' ] },
  116. model: 'complexInfoBoxTitle'
  117. } )
  118. .elementToElement( {
  119. view: { name: 'div', classes: [ 'info-box-content' ] },
  120. model: 'complexInfoBoxContent'
  121. } );
  122. // The downcast conversion must be split as you need a widget in the editing pipeline.
  123. conversion.for( 'downcast' ).elementToElement( {
  124. model: 'complexInfoBox',
  125. view: downcastInfoBox( editor ),
  126. triggerBy: {
  127. attributes: [ 'infoBoxType', 'infoBoxURL' ],
  128. children: [ 'complexInfoBoxContent' ]
  129. }
  130. } );
  131. }
  132. _defineSchema( editor ) {
  133. const schema = editor.model.schema;
  134. // The main element with attributes for type and URL:
  135. schema.register( 'complexInfoBox', {
  136. allowWhere: '$block',
  137. isObject: true,
  138. allowAttributes: [ 'infoBoxType', 'infoBoxURL' ]
  139. } );
  140. // A text-only title.
  141. schema.register( 'complexInfoBoxTitle', {
  142. isLimit: true,
  143. allowIn: 'complexInfoBox'
  144. } );
  145. // Allow text in title...
  146. schema.extend( '$text', { allowIn: 'complexInfoBoxTitle' } );
  147. // ...but disallow any text attribute inside.
  148. schema.addAttributeCheck( context => {
  149. if ( context.endsWith( 'complexInfoBoxTitle $text' ) ) {
  150. return false;
  151. }
  152. } );
  153. // A content block which can have any content allowed in $root.
  154. schema.register( 'complexInfoBoxContent', {
  155. isLimit: true,
  156. allowIn: 'complexInfoBox',
  157. allowContentOf: '$root'
  158. } );
  159. }
  160. }
  161. ClassicEditor
  162. .create( document.querySelector( '#editor-element-reconversion' ), {
  163. cloudServices: CS_CONFIG,
  164. extraPlugins: [ ComplexInfoBox ],
  165. image: {
  166. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  167. },
  168. table: {
  169. contentToolbar: [
  170. 'tableColumn',
  171. 'tableRow',
  172. 'mergeTableCells'
  173. ]
  174. },
  175. toolbar: {
  176. items: [ 'heading', '|', 'bold', 'italic' ],
  177. viewportTopOffset: window.getViewportTopOffsetConfig()
  178. }
  179. } )
  180. .then( editor => {
  181. window.editor = editor;
  182. } )
  183. .catch( err => {
  184. console.error( err );
  185. } );