extending-content-custom-element-converter.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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, document */
  6. import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
  7. class InfoBox {
  8. constructor( editor ) {
  9. // Schema definition.
  10. editor.model.schema.register( 'infoBox', {
  11. allowWhere: '$block',
  12. allowContentOf: '$root',
  13. isObject: true,
  14. allowAttributes: [ 'infoBoxType' ]
  15. } );
  16. // Upcast converter.
  17. editor.conversion.for( 'upcast' )
  18. .add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
  19. // The downcast conversion must be split as you need a widget in the editing pipeline.
  20. editor.conversion.for( 'editingDowncast' )
  21. .add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
  22. editor.conversion.for( 'dataDowncast' )
  23. .add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
  24. }
  25. }
  26. function upcastConverter( event, data, conversionApi ) {
  27. const viewInfoBox = data.viewItem;
  28. // Detect that a view element is an info box <div>.
  29. // Otherwise, it should be handled by another converter.
  30. if ( !viewInfoBox.hasClass( 'info-box' ) ) {
  31. return;
  32. }
  33. // Create the model structure.
  34. const modelElement = conversionApi.writer.createElement( 'infoBox', {
  35. infoBoxType: getTypeFromViewElement( viewInfoBox )
  36. } );
  37. // Try to safely insert the element into the model structure.
  38. // If `safeInsert()` returns `false`, the element cannot be safely inserted
  39. // into the content and the conversion process must stop.
  40. // This may happen if the data that you are converting has an incorrect structure
  41. // (e.g. it was copied from an external website).
  42. if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
  43. return;
  44. }
  45. // Mark the info box <div> as handled by this converter.
  46. conversionApi.consumable.consume( viewInfoBox, { name: true } );
  47. // Let us assume that the HTML structure is always the same.
  48. // Note: For full bulletproofing this converter, you should also check
  49. // whether these elements are the right ones.
  50. const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
  51. const viewInfoBoxContent = viewInfoBox.getChild( 1 );
  52. // Mark info box inner elements (title and content <div>s) as handled by this converter.
  53. conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
  54. conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
  55. // Let the editor handle the children of <div class="info-box-content">.
  56. conversionApi.convertChildren( viewInfoBoxContent, modelElement );
  57. // Finally, update the conversion's modelRange and modelCursor.
  58. conversionApi.updateConversionResult( modelElement, data );
  59. }
  60. function editingDowncastConverter( event, data, conversionApi ) {
  61. let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  62. // Decorate view items as a widget and widget editable area.
  63. infoBox = toWidget( infoBox, conversionApi.writer, { label: 'info box widget' } );
  64. infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
  65. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  66. }
  67. function dataDowncastConverter( event, data, conversionApi ) {
  68. const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  69. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  70. }
  71. function createViewElements( data, conversionApi ) {
  72. const type = data.item.getAttribute( 'infoBoxType' );
  73. const infoBox = conversionApi.writer.createContainerElement( 'div', {
  74. class: `info-box info-box-${ type.toLowerCase() }`
  75. } );
  76. const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
  77. class: 'info-box-content'
  78. } );
  79. const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
  80. { class: 'info-box-title' },
  81. function( domDocument ) {
  82. const domElement = this.toDomElement( domDocument );
  83. domElement.innerText = type;
  84. return domElement;
  85. } );
  86. return { infoBox, infoBoxContent, infoBoxTitle };
  87. }
  88. function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
  89. conversionApi.consumable.consume( data.item, 'insert' );
  90. conversionApi.writer.insert(
  91. conversionApi.writer.createPositionAt( infoBox, 0 ),
  92. infoBoxTitle
  93. );
  94. conversionApi.writer.insert(
  95. conversionApi.writer.createPositionAt( infoBox, 1 ),
  96. infoBoxContent
  97. );
  98. // The default mapping between the model <infoBox> and its view representation.
  99. conversionApi.mapper.bindElements( data.item, infoBox );
  100. // However, since the model <infoBox> content needs to end up in the inner
  101. // <div class="info-box-content">, you need to bind one with another overriding
  102. // a part of the default binding.
  103. conversionApi.mapper.bindElements( data.item, infoBoxContent );
  104. conversionApi.writer.insert(
  105. conversionApi.mapper.toViewPosition( data.range.start ),
  106. infoBox
  107. );
  108. }
  109. ClassicEditor
  110. .create( document.querySelector( '#editor-custom-element-converter' ), {
  111. cloudServices: CS_CONFIG,
  112. extraPlugins: [ InfoBox ],
  113. image: {
  114. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  115. },
  116. table: {
  117. contentToolbar: [
  118. 'tableColumn',
  119. 'tableRow',
  120. 'mergeTableCells'
  121. ]
  122. },
  123. toolbar: {
  124. viewportTopOffset: window.getViewportTopOffsetConfig()
  125. }
  126. } )
  127. .then( editor => {
  128. window.editor = editor;
  129. } )
  130. .catch( err => {
  131. console.error( err.stack );
  132. } );
  133. function getTypeFromViewElement( viewElement ) {
  134. if ( viewElement.hasClass( 'info-box-info' ) ) {
  135. return 'Info';
  136. }
  137. if ( viewElement.hasClass( 'info-box-warning' ) ) {
  138. return 'Warning';
  139. }
  140. return 'None';
  141. }