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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 we 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 view element is an info-box div. Otherwise, it should be handled by another converter.
  29. if ( !viewInfoBox.hasClass( 'info-box' ) ) {
  30. return;
  31. }
  32. // Create a model structure.
  33. const modelElement = conversionApi.writer.createElement( 'infoBox', {
  34. infoBoxType: getTypeFromViewElement( viewInfoBox )
  35. } );
  36. // Try to safely insert element - if it returns false the element can't be safely inserted
  37. // into the content, and the conversion process must stop.
  38. if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
  39. return;
  40. }
  41. // Mark info-box div as handled by this converter.
  42. conversionApi.consumable.consume( viewInfoBox, { name: true } );
  43. // Let's assume that the HTML structure is always the same.
  44. const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
  45. const viewInfoBoxContent = viewInfoBox.getChild( 1 );
  46. // Mark info-box inner elements as handled by this converter.
  47. conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
  48. conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
  49. // Let the editor handle children of the info-box content conversion.
  50. conversionApi.convertChildren( viewInfoBoxContent, modelElement );
  51. // Conversion requires updating result data structure properly.
  52. conversionApi.updateConversionResult( modelElement, data );
  53. }
  54. function editingDowncastConverter( event, data, conversionApi ) {
  55. let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  56. // Decorate view items as widgets.
  57. infoBox = toWidget( infoBox, conversionApi.writer, { label: 'simple box widget' } );
  58. infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
  59. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  60. }
  61. function dataDowncastConverter( event, data, conversionApi ) {
  62. const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  63. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  64. }
  65. function createViewElements( data, conversionApi ) {
  66. const type = data.item.getAttribute( 'infoBoxType' );
  67. const infoBox = conversionApi.writer.createContainerElement( 'div', {
  68. class: `info-box info-box-${ type.toLowerCase() }`
  69. } );
  70. const infoBoxContent = conversionApi.writer.createEditableElement( 'div', {
  71. class: 'info-box-content'
  72. } );
  73. const infoBoxTitle = conversionApi.writer.createUIElement( 'div',
  74. { class: 'info-box-title' },
  75. function( domDocument ) {
  76. const domElement = this.toDomElement( domDocument );
  77. domElement.innerText = type;
  78. return domElement;
  79. } );
  80. return { infoBox, infoBoxContent, infoBoxTitle };
  81. }
  82. function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
  83. conversionApi.consumable.consume( data.item, 'insert' );
  84. conversionApi.writer.insert(
  85. conversionApi.writer.createPositionAt( infoBox, 0 ),
  86. infoBoxTitle
  87. );
  88. conversionApi.writer.insert(
  89. conversionApi.writer.createPositionAt( infoBox, 1 ),
  90. infoBoxContent
  91. );
  92. conversionApi.mapper.bindElements( data.item, infoBox );
  93. conversionApi.mapper.bindElements( data.item, infoBoxContent );
  94. conversionApi.writer.insert(
  95. conversionApi.mapper.toViewPosition( data.range.start ),
  96. infoBox
  97. );
  98. }
  99. ClassicEditor
  100. .create( document.querySelector( '#editor-custom-element-converter' ), {
  101. cloudServices: CS_CONFIG,
  102. extraPlugins: [ InfoBox ],
  103. image: {
  104. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  105. },
  106. table: {
  107. contentToolbar: [
  108. 'tableColumn',
  109. 'tableRow',
  110. 'mergeTableCells'
  111. ]
  112. },
  113. toolbar: {
  114. viewportTopOffset: window.getViewportTopOffsetConfig()
  115. }
  116. } )
  117. .then( editor => {
  118. window.editor = editor;
  119. } )
  120. .catch( err => {
  121. console.error( err.stack );
  122. } );
  123. function getTypeFromViewElement( viewElement ) {
  124. const classNames = [ ...viewElement.getClassNames() ];
  125. if ( classNames.includes( 'info-box-info' ) ) {
  126. return 'Info';
  127. }
  128. if ( classNames.includes( 'info-box-warning' ) ) {
  129. return 'Warning';
  130. }
  131. return 'None';
  132. }