article.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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 boxElements = [ ...differ.getChanges() ]
  39. .filter( ( { type } ) => type === 'attribute' || type === 'insert' || type === 'remove' )
  40. .map( ( { range, position } ) => range && range.start.nodeAfter || position && position.parent )
  41. .filter( element => element && element.is( 'element', 'box' ) );
  42. const boxToRefresh = new Set( boxElements );
  43. [ ...boxToRefresh.values() ].forEach( box => differ.refreshItem( box ) );
  44. return boxToRefresh.size > 1;
  45. }
  46. function getChildren( editor, viewElement ) {
  47. return [ ...( editor.editing.view.createRangeIn( viewElement ) ) ]
  48. .filter( ( { type } ) => type === 'elementStart' )
  49. .map( ( { item } ) => item );
  50. }
  51. function getBoxUpcastConverter( editor ) {
  52. return dispatcher => dispatcher.on( 'element:div', ( event, data, conversionApi ) => {
  53. const viewElement = data.viewItem;
  54. const writer = conversionApi.writer;
  55. if ( !viewElement.hasClass( 'box' ) ) {
  56. return;
  57. }
  58. const box = writer.createElement( 'box' );
  59. if ( !conversionApi.safeInsert( box, data.modelCursor ) ) {
  60. return;
  61. }
  62. const elements = getChildren( editor, viewElement );
  63. const fields = elements.filter( byClassName( 'box-content-field' ) );
  64. const metaElements = elements.filter( byClassName( 'box-meta' ) );
  65. const meta = metaElements.map( mapMeta( editor ) ).reduce( ( prev, current ) => Object.assign( prev, current ), {} );
  66. writer.setAttribute( 'meta', meta, box );
  67. for ( const field of fields ) {
  68. const boxField = writer.createElement( 'boxField' );
  69. conversionApi.safeInsert( boxField, writer.createPositionAt( box, field.index ) );
  70. conversionApi.convertChildren( field, boxField );
  71. }
  72. conversionApi.consumable.consume( viewElement, { name: true } );
  73. elements.map( element => {
  74. conversionApi.consumable.consume( element, { name: true } );
  75. } );
  76. conversionApi.updateConversionResult( box, data );
  77. } );
  78. }
  79. function downcastBox( modelElement, conversionApi ) {
  80. console.log( 'downcastBox' );
  81. const { writer } = conversionApi;
  82. const viewBox = writer.createContainerElement( 'div', { class: 'box' } );
  83. const contentWrap = writer.createContainerElement( 'div', { class: 'box-content' } );
  84. writer.insert( writer.createPositionAt( viewBox, 0 ), contentWrap );
  85. for ( const [ meta, metaValue ] of Object.entries( modelElement.getAttribute( 'meta' ) ) ) {
  86. if ( meta === 'header' ) {
  87. const header = writer.createRawElement( 'div', {
  88. class: 'box-meta box-meta-header'
  89. }, function( domElement ) {
  90. domElement.innerHTML = `<div class="box-meta-header-title"><h2>${ metaValue.title }</h2></div>`;
  91. } );
  92. writer.insert( writer.createPositionBefore( contentWrap ), header );
  93. }
  94. if ( meta === 'author' ) {
  95. const author = writer.createRawElement( 'div', {
  96. class: 'box-meta box-meta-author'
  97. }, domElement => {
  98. domElement.innerHTML = `<a href="${ metaValue.website }">${ metaValue.name }</a>`;
  99. } );
  100. writer.insert( writer.createPositionAfter( contentWrap ), author );
  101. }
  102. }
  103. for ( const field of modelElement.getChildren() ) {
  104. const viewField = writer.createContainerElement( 'div', { class: 'box-content-field' } );
  105. writer.insert( writer.createPositionAt( contentWrap, field.index ), viewField );
  106. conversionApi.mapper.bindElements( field, viewField );
  107. }
  108. conversionApi.mapper.bindElements( modelElement, viewBox );
  109. return viewBox;
  110. }
  111. function addButton( editor, uiName, label, callback ) {
  112. editor.ui.componentFactory.add( uiName, locale => {
  113. const view = new ButtonView( locale );
  114. view.set( { label, withText: true } );
  115. view.listenTo( view, 'execute', () => {
  116. const parent = editor.model.document.selection.getFirstPosition().parent;
  117. const boxField = parent.findAncestor( 'boxField' );
  118. if ( !boxField ) {
  119. return;
  120. }
  121. editor.model.change( writer => callback( writer, boxField.findAncestor( 'box' ), boxField ) );
  122. } );
  123. return view;
  124. } );
  125. }
  126. function addBoxMetaButton( editor, uiName, label, updateWith ) {
  127. addButton( editor, uiName, label, ( writer, box ) => {
  128. writer.setAttribute( 'meta', {
  129. ...box.getAttribute( 'meta' ),
  130. ...updateWith()
  131. }, box );
  132. } );
  133. }
  134. function Box( editor ) {
  135. editor.model.schema.register( 'box', {
  136. allowIn: '$root',
  137. isObject: true,
  138. isSelectable: true,
  139. allowAttributes: [ 'infoBoxMeta' ]
  140. } );
  141. editor.model.schema.register( 'boxField', {
  142. allowContentOf: '$root',
  143. allowIn: 'box',
  144. isLimit: true
  145. } );
  146. editor.conversion.for( 'upcast' ).add( getBoxUpcastConverter( editor ) );
  147. editor.conversion.for( 'downcast' ).elementToElement( {
  148. model: 'box',
  149. view: downcastBox
  150. } );
  151. editor.model.document.registerPostFixer( () => boxRefresh( editor.model ) );
  152. addBoxMetaButton( editor, 'boxTitle', 'Box title', () => ( {
  153. header: { title: `Random title no. ${ getRandom() }.` }
  154. } ) );
  155. addBoxMetaButton( editor, 'boxAuthor', 'Box author', () => ( {
  156. author: {
  157. website: `www.example.com/${ getRandom() }`,
  158. name: `Random author no. ${ getRandom() }`
  159. }
  160. } ) );
  161. addButton( editor, 'addBoxField', '+', ( writer, box, boxField ) => {
  162. const newBoxField = writer.createElement( 'boxField' );
  163. writer.insert( newBoxField, box, boxField.index );
  164. writer.insert( writer.createElement( 'paragraph' ), newBoxField, 0 );
  165. } );
  166. addButton( editor, 'removeBoxField', '-', ( writer, box, boxField ) => {
  167. writer.remove( boxField );
  168. } );
  169. }
  170. ClassicEditor
  171. .create( document.querySelector( '#editor' ), {
  172. plugins: [ ArticlePluginSet, Box, DecorateViewItems ],
  173. toolbar: [
  174. 'heading',
  175. '|',
  176. 'boxTitle',
  177. 'boxAuthor',
  178. 'addBoxField',
  179. 'removeBoxField',
  180. '|',
  181. 'bold',
  182. 'italic',
  183. 'link',
  184. 'bulletedList',
  185. 'numberedList',
  186. '|',
  187. 'outdent',
  188. 'indent',
  189. '|',
  190. 'blockQuote',
  191. 'insertTable',
  192. 'mediaEmbed',
  193. 'undo',
  194. 'redo'
  195. ],
  196. image: {
  197. toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  198. },
  199. table: {
  200. contentToolbar: [
  201. 'tableColumn',
  202. 'tableRow',
  203. 'mergeTableCells'
  204. ]
  205. }
  206. } )
  207. .then( editor => {
  208. window.editor = editor;
  209. } )
  210. .catch( err => {
  211. console.error( err.stack );
  212. } );
  213. function DecorateViewItems( editor ) {
  214. editor.conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
  215. if ( data.item.is( 'element' ) ) {
  216. const viewItem = editor.editing.mapper.toViewElement( data.item );
  217. conversionApi.writer.setAttribute( 'data-rendered-id', getNextRenderId(), viewItem );
  218. }
  219. }, { priority: 'lowest' } ) );
  220. }