element-reconversion-demo.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals ClassicEditor, 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. writer.setAttribute( 'infoBoxURL', 'https://ckeditor.com', complexInfoBox );
  21. return complexInfoBox;
  22. };
  23. const renderActionsView = ( editor, modelElement ) => function( domElement ) {
  24. const domDocument = domElement.ownerDocument;
  25. const urlButton = createElement( domDocument, 'button', {}, 'Set URL' );
  26. urlButton.addEventListener( 'click', () => {
  27. // eslint-disable-next-line no-alert
  28. const newURL = prompt( 'Set URL', modelElement.getAttribute( 'infoBoxURL' ) || '' );
  29. editor.model.change( writer => {
  30. writer.setAttribute( 'infoBoxURL', newURL, modelElement );
  31. } );
  32. } );
  33. const currentType = modelElement.getAttribute( 'infoBoxType' );
  34. const newType = currentType === 'info' ? 'warning' : 'info';
  35. const typeButton = createElement( domDocument, 'button', {}, `Change to ${ newType }` );
  36. typeButton.addEventListener( 'click', () => {
  37. editor.model.change( writer => {
  38. writer.setAttribute( 'infoBoxType', newType, modelElement );
  39. } );
  40. } );
  41. domElement.appendChild( urlButton );
  42. domElement.appendChild( typeButton );
  43. };
  44. // TODO: simplify to hide complexity
  45. const downcastInfoBox = editor => {
  46. return ( modelElement, { writer, consumable, mapper } ) => {
  47. const complexInfoBoxView = writer.createContainerElement( 'div', {
  48. class: 'info-box'
  49. } );
  50. const type = modelElement.getAttribute( 'infoBoxType' ) || 'info';
  51. writer.addClass( `info-box-${ type }`, complexInfoBoxView );
  52. const actionsView = writer.createRawElement( 'div', {
  53. class: 'info-box-actions',
  54. contenteditable: 'false', // Prevent editing of the element:
  55. 'data-cke-ignore-events': 'true' // Allows using custom UI elements inside editing view.
  56. }, renderActionsView( editor, modelElement ) );
  57. writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), actionsView );
  58. for ( const child of modelElement.getChildren() ) {
  59. const childView = writer.createContainerElement( 'div' );
  60. if ( child.is( 'element', 'complexInfoBoxTitle' ) ) {
  61. writer.addClass( 'info-box-title', childView );
  62. } else {
  63. writer.addClass( 'info-box-content', childView );
  64. }
  65. consumable.consume( child, 'insert' );
  66. mapper.bindElements( child, childView );
  67. writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), childView );
  68. }
  69. return complexInfoBoxView;
  70. };
  71. };
  72. class ComplexInfoBox {
  73. constructor( editor ) {
  74. this._defineSchema( editor );
  75. this._defineConversion( editor );
  76. }
  77. _defineConversion( editor ) {
  78. editor.conversion.for( 'upcast' )
  79. .elementToElement( {
  80. view: { name: 'div', classes: [ 'info-box' ] },
  81. model: upcastInfoBox
  82. } )
  83. .elementToElement( {
  84. view: { name: 'div', classes: [ 'info-box-title' ] },
  85. model: 'complexInfoBoxTitle'
  86. } )
  87. .elementToElement( {
  88. view: { name: 'div', classes: [ 'info-box-content' ] },
  89. model: 'complexInfoBoxContent'
  90. } );
  91. // The downcast conversion must be split as you need a widget in the editing pipeline.
  92. editor.conversion.for( 'downcast' ).elementToElement( {
  93. model: 'complexInfoBox',
  94. view: downcastInfoBox( editor ),
  95. triggerBy: {
  96. attributes: [ 'infoBoxType', 'infoBoxURL' ],
  97. children: [ 'complexInfoBoxContent' ]
  98. }
  99. } );
  100. }
  101. _defineSchema( editor ) {
  102. // The main element with attributes for type and URL:
  103. editor.model.schema.register( 'complexInfoBox', {
  104. allowWhere: '$block',
  105. allowContentOf: '$root',
  106. isObject: true,
  107. allowAttributes: [ 'infoBoxType', 'infoBoxURL' ]
  108. } );
  109. // A text-only title.
  110. editor.model.schema.register( 'complexInfoBoxTitle', {
  111. isLimit: true,
  112. allowIn: 'complexInfoBox'
  113. } );
  114. editor.model.schema.extend( '$text', { allowIn: 'complexInfoBoxTitle' } );
  115. // A content which can have any content allowed in $root.
  116. editor.model.schema.register( 'complexInfoBoxContent', {
  117. isLimit: true,
  118. allowIn: 'complexInfoBox',
  119. allowContentOf: '$root'
  120. } );
  121. }
  122. }
  123. ClassicEditor
  124. .create( document.querySelector( '#editor-element-reconversion' ), {
  125. cloudServices: CS_CONFIG,
  126. extraPlugins: [ ComplexInfoBox ],
  127. image: {
  128. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  129. },
  130. table: {
  131. contentToolbar: [
  132. 'tableColumn',
  133. 'tableRow',
  134. 'mergeTableCells'
  135. ]
  136. },
  137. toolbar: {
  138. items: [ 'heading', '|', 'bold', 'italic' ],
  139. viewportTopOffset: window.getViewportTopOffsetConfig()
  140. }
  141. } )
  142. .then( editor => {
  143. window.editor = editor;
  144. } )
  145. .catch( err => {
  146. console.error( err );
  147. } );