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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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. editor.model.schema.register( 'infoBox', {
  10. allowWhere: '$block',
  11. allowContentOf: '$root',
  12. isObject: true,
  13. allowAttributes: [ 'infoBoxType' ]
  14. } );
  15. editor.conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:div', upcastConverter ) );
  16. // The downcast conversion must be split as we need a widget in the editing pipeline.
  17. editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'insert:infoBox', editingDowncastConverter ) );
  18. editor.conversion.for( 'dataDowncast' ).add( dispatcher => dispatcher.on( 'insert:infoBox', dataDowncastConverter ) );
  19. }
  20. }
  21. function upcastConverter( event, data, conversionApi ) {
  22. const viewInfoBox = data.viewItem;
  23. // Detect that view element is an info-box div. Otherwise, it should be handled by another converter.
  24. if ( !viewInfoBox.hasClass( 'info-box' ) ) {
  25. return;
  26. }
  27. // Create a model structure.
  28. const modelElement = conversionApi.writer.createElement( 'infoBox', {
  29. infoBoxType: getTypeFromViewElement( viewInfoBox )
  30. } );
  31. // Try to safely insert element - if it returns false the element can't be safely inserted into the content,
  32. // and the conversion process must stop.
  33. if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
  34. return;
  35. }
  36. // Mark info-box div as handled by this converter.
  37. conversionApi.consumable.consume( viewInfoBox, { name: true } );
  38. // Let's assume that the HTML structure is always the same.
  39. // If you don't control the order of view elements a more sophisticated search might be needed.
  40. const viewInfoBoxTitle = viewInfoBox.getChild( 0 );
  41. const viewInfoBoxContent = viewInfoBox.getChild( 1 );
  42. // Mark info-box inner elements as handled by this converter.
  43. conversionApi.consumable.consume( viewInfoBoxTitle, { name: true } );
  44. conversionApi.consumable.consume( viewInfoBoxContent, { name: true } );
  45. // Let the editor handle children of the info-box content conversion.
  46. conversionApi.convertChildren( viewInfoBoxContent, modelElement );
  47. // Conversion requires updating result data structure properly.
  48. conversionApi.updateConversionResult( modelElement, data );
  49. }
  50. function editingDowncastConverter( event, data, conversionApi ) {
  51. let { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  52. // Decorate view items as widgets.
  53. infoBox = toWidget( infoBox, conversionApi.writer, { label: 'simple box widget' } );
  54. infoBoxContent = toWidgetEditable( infoBoxContent, conversionApi.writer );
  55. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  56. }
  57. function dataDowncastConverter( event, data, conversionApi ) {
  58. const { infoBox, infoBoxContent, infoBoxTitle } = createViewElements( data, conversionApi );
  59. insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent );
  60. }
  61. function createViewElements( data, conversionApi ) {
  62. const type = data.item.getAttribute( 'infoBoxType' );
  63. const infoBox = conversionApi.writer.createContainerElement( 'div', { class: `info-box info-box-${ type.toLowerCase() }` } );
  64. const infoBoxContent = conversionApi.writer.createEditableElement( 'div', { class: 'info-box-content' } );
  65. const infoBoxTitle = conversionApi.writer.createUIElement( 'div', { class: 'info-box-title' }, function( domDocument ) {
  66. const domElement = this.toDomElement( domDocument );
  67. domElement.innerText = type;
  68. return domElement;
  69. } );
  70. return { infoBox, infoBoxContent, infoBoxTitle };
  71. }
  72. function insertViewElements( data, conversionApi, infoBox, infoBoxTitle, infoBoxContent ) {
  73. conversionApi.consumable.consume( data.item, 'insert' );
  74. conversionApi.writer.insert( conversionApi.writer.createPositionAt( infoBox, 0 ), infoBoxTitle );
  75. conversionApi.writer.insert( conversionApi.writer.createPositionAt( infoBox, 1 ), infoBoxContent );
  76. conversionApi.mapper.bindElements( data.item, infoBox );
  77. conversionApi.mapper.bindElements( data.item, infoBoxContent );
  78. conversionApi.writer.insert( conversionApi.mapper.toViewPosition( data.range.start ), infoBox );
  79. }
  80. ClassicEditor
  81. .create( document.querySelector( '#editor-custom-element-converter' ), {
  82. cloudServices: CS_CONFIG,
  83. extraPlugins: [ InfoBox ],
  84. image: {
  85. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  86. },
  87. table: {
  88. contentToolbar: [
  89. 'tableColumn',
  90. 'tableRow',
  91. 'mergeTableCells'
  92. ]
  93. },
  94. toolbar: {
  95. viewportTopOffset: window.getViewportTopOffsetConfig()
  96. }
  97. } )
  98. .then( editor => {
  99. window.editor = editor;
  100. } )
  101. .catch( err => {
  102. console.error( err.stack );
  103. } );
  104. function getTypeFromViewElement( viewElement ) {
  105. const classNames = [ ...viewElement.getClassNames() ];
  106. if ( classNames.includes( 'info-box-info' ) ) {
  107. return 'Info';
  108. }
  109. if ( classNames.includes( 'info-box-warning' ) ) {
  110. return 'Warning';
  111. }
  112. return 'None';
  113. }