8
0

article.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals console, window, document */
  6. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  7. import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
  8. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  9. const byClassName = className => element => element.hasClass( className );
  10. const getRandom = () => parseInt( Math.random() * 1000 );
  11. let renderId = 0;
  12. const getNextRenderId = () => renderId++;
  13. function mapMeta( editor ) {
  14. return metaElement => {
  15. if ( metaElement.hasClass( 'box-meta-header' ) ) {
  16. const title = getChildren( editor, metaElement )
  17. .filter( byClassName( 'box-meta-header-title' ) )
  18. .pop().getChild( 0 ).getChild( 0 ).data;
  19. return {
  20. header: {
  21. title
  22. }
  23. };
  24. }
  25. if ( metaElement.hasClass( 'box-meta-author' ) ) {
  26. const link = metaElement.getChild( 0 );
  27. return {
  28. author: {
  29. name: link.getChild( 0 ).data,
  30. website: link.getAttribute( 'href' )
  31. }
  32. };
  33. }
  34. };
  35. }
  36. function boxRefresh( model ) {
  37. const differ = model.document.differ;
  38. const changes = differ.getChanges();
  39. console.log( `boxRefresh() size: ${ changes.length }` );
  40. const boxElements = [ ...changes ]
  41. .filter( ( { type } ) => type === 'attribute' || type === 'insert' || type === 'remove' )
  42. .map( ( { range, position } ) => range && range.start.nodeAfter || position && position.parent )
  43. .filter( element => element && element.is( 'element', 'box' ) );
  44. const boxToRefresh = new Set( boxElements );
  45. console.group( 'refreshItem' );
  46. [ ...boxToRefresh.values() ].forEach( box => differ.refreshItem( box ) );
  47. console.groupEnd();
  48. return boxToRefresh.size > 1;
  49. }
  50. function getChildren( editor, viewElement ) {
  51. return [ ...( editor.editing.view.createRangeIn( viewElement ) ) ]
  52. .filter( ( { type } ) => type === 'elementStart' )
  53. .map( ( { item } ) => item );
  54. }
  55. function getBoxUpcastConverter( editor ) {
  56. return dispatcher => dispatcher.on( 'element:div', ( event, data, conversionApi ) => {
  57. const viewElement = data.viewItem;
  58. const writer = conversionApi.writer;
  59. if ( !viewElement.hasClass( 'box' ) ) {
  60. return;
  61. }
  62. const box = writer.createElement( 'box' );
  63. if ( !conversionApi.safeInsert( box, data.modelCursor ) ) {
  64. return;
  65. }
  66. const elements = getChildren( editor, viewElement );
  67. const fields = elements.filter( byClassName( 'box-content-field' ) );
  68. const metaElements = elements.filter( byClassName( 'box-meta' ) );
  69. const meta = metaElements.map( mapMeta( editor ) ).reduce( ( prev, current ) => Object.assign( prev, current ), {} );
  70. writer.setAttribute( 'meta', meta, box );
  71. for ( const field of fields ) {
  72. const boxField = writer.createElement( 'boxField' );
  73. conversionApi.safeInsert( boxField, writer.createPositionAt( box, field.index ) );
  74. conversionApi.convertChildren( field, boxField );
  75. }
  76. conversionApi.consumable.consume( viewElement, { name: true } );
  77. elements.map( element => {
  78. conversionApi.consumable.consume( element, { name: true } );
  79. } );
  80. conversionApi.updateConversionResult( box, data );
  81. } );
  82. }
  83. function downcastBox( modelElement, conversionApi ) {
  84. console.log( 'downcastBox' );
  85. const { writer } = conversionApi;
  86. const viewBox = writer.createContainerElement( 'div', { class: 'box' } );
  87. conversionApi.mapper.bindElements( modelElement, viewBox );
  88. const contentWrap = writer.createContainerElement( 'div', { class: 'box-content' } );
  89. writer.insert( writer.createPositionAt( viewBox, 0 ), contentWrap );
  90. for ( const [ meta, metaValue ] of Object.entries( modelElement.getAttribute( 'meta' ) ) ) {
  91. if ( meta === 'header' ) {
  92. const header = writer.createRawElement( 'div', {
  93. class: 'box-meta box-meta-header'
  94. }, function( domElement ) {
  95. domElement.innerHTML = `<div class="box-meta-header-title"><h2>${ metaValue.title }</h2></div>`;
  96. } );
  97. writer.insert( writer.createPositionBefore( contentWrap ), header );
  98. }
  99. if ( meta === 'author' ) {
  100. const author = writer.createRawElement( 'div', {
  101. class: 'box-meta box-meta-author'
  102. }, domElement => {
  103. domElement.innerHTML = `<a href="${ metaValue.website }">${ metaValue.name }</a>`;
  104. } );
  105. writer.insert( writer.createPositionAfter( contentWrap ), author );
  106. }
  107. }
  108. for ( const field of modelElement.getChildren() ) {
  109. const viewField = writer.createContainerElement( 'div', { class: 'box-content-field' } );
  110. writer.insert( writer.createPositionAt( contentWrap, field.index ), viewField );
  111. conversionApi.mapper.bindElements( field, viewField );
  112. }
  113. // At this point we're inserting whole "component". Equivalent to (JSX-like notation):
  114. //
  115. // "rendered" view Mapping/source
  116. //
  117. // <div:container class="box"> <-- top-level box
  118. // <div:raw class="box-meta box-meta-header">...</div:raw> box[meta.header]
  119. // <div:container class="box-content">
  120. // <div:container class="box-content-field">...</div:container> <-- this is "slot" boxField
  121. // ... many
  122. // <div:container class="box-content-field">...</div:container> <-- this is "slot" boxField
  123. // </div:container>
  124. // <div:raw class="box-meta box-meta-author">...</div:raw> box[meta.author]
  125. // </div:container>
  126. return viewBox;
  127. }
  128. function addButton( editor, uiName, label, callback ) {
  129. editor.ui.componentFactory.add( uiName, locale => {
  130. const view = new ButtonView( locale );
  131. view.set( { label, withText: true } );
  132. view.listenTo( view, 'execute', () => {
  133. const parent = editor.model.document.selection.getFirstPosition().parent;
  134. const boxField = parent.findAncestor( 'boxField' );
  135. if ( !boxField ) {
  136. return;
  137. }
  138. editor.model.change( writer => callback( writer, boxField.findAncestor( 'box' ), boxField ) );
  139. } );
  140. return view;
  141. } );
  142. }
  143. function addBoxMetaButton( editor, uiName, label, updateWith ) {
  144. addButton( editor, uiName, label, ( writer, box ) => {
  145. writer.setAttribute( 'meta', {
  146. ...box.getAttribute( 'meta' ),
  147. ...updateWith()
  148. }, box );
  149. } );
  150. }
  151. function Box( editor ) {
  152. editor.model.schema.register( 'box', {
  153. allowIn: '$root',
  154. isObject: true,
  155. isSelectable: true,
  156. allowAttributes: [ 'infoBoxMeta' ]
  157. } );
  158. editor.model.schema.register( 'boxField', {
  159. allowContentOf: '$root',
  160. allowIn: 'box',
  161. isLimit: true
  162. } );
  163. editor.conversion.for( 'upcast' ).add( getBoxUpcastConverter( editor ) );
  164. editor.conversion.for( 'downcast' ).elementToElement( {
  165. model: 'box',
  166. view: downcastBox
  167. } );
  168. editor.model.document.registerPostFixer( () => boxRefresh( editor.model ) );
  169. addBoxMetaButton( editor, 'boxTitle', 'Box title', () => ( {
  170. header: { title: `Random title no. ${ getRandom() }.` }
  171. } ) );
  172. addBoxMetaButton( editor, 'boxAuthor', 'Box author', () => ( {
  173. author: {
  174. website: `www.example.com/${ getRandom() }`,
  175. name: `Random author no. ${ getRandom() }`
  176. }
  177. } ) );
  178. addButton( editor, 'addBoxField', '+', ( writer, box, boxField ) => {
  179. const newBoxField = writer.createElement( 'boxField' );
  180. writer.insert( newBoxField, box, boxField.index );
  181. writer.insert( writer.createElement( 'paragraph' ), newBoxField, 0 );
  182. } );
  183. addButton( editor, 'removeBoxField', '-', ( writer, box, boxField ) => {
  184. writer.remove( boxField );
  185. } );
  186. }
  187. ClassicEditor
  188. .create( document.querySelector( '#editor' ), {
  189. plugins: [ ArticlePluginSet, Box, DecorateViewItems ],
  190. toolbar: [
  191. 'heading',
  192. '|',
  193. 'boxTitle',
  194. 'boxAuthor',
  195. 'addBoxField',
  196. 'removeBoxField',
  197. '|',
  198. 'bold',
  199. 'italic',
  200. 'link',
  201. 'bulletedList',
  202. 'numberedList',
  203. '|',
  204. 'outdent',
  205. 'indent',
  206. '|',
  207. 'blockQuote',
  208. 'insertTable',
  209. 'mediaEmbed',
  210. 'undo',
  211. 'redo'
  212. ],
  213. image: {
  214. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  215. },
  216. table: {
  217. contentToolbar: [
  218. 'tableColumn',
  219. 'tableRow',
  220. 'mergeTableCells'
  221. ]
  222. }
  223. } )
  224. .then( editor => {
  225. window.editor = editor;
  226. } )
  227. .catch( err => {
  228. console.error( err.stack );
  229. } );
  230. function DecorateViewItems( editor ) {
  231. editor.conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
  232. if ( data.item.is( 'element' ) ) {
  233. const viewItem = editor.editing.mapper.toViewElement( data.item );
  234. conversionApi.writer.setAttribute( 'data-rendered-id', getNextRenderId(), viewItem );
  235. }
  236. }, { priority: 'lowest' } ) );
  237. }