| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /* globals
- ClassicEditor, Plugin, ButtonView, Command, toWidget, toWidgetEditable, createElement, findOptimalInsertionPosition,
- console, window, prompt, document
- */
- import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
- /**
- * Helper for extracting the side card type from a view element based on its CSS class.
- */
- const getTypeFromViewElement = viewElement => {
- for ( const type of [ 'default', 'alternate' ] ) {
- if ( viewElement.hasClass( `side-card-${ type }` ) ) {
- return type;
- }
- }
- return 'default';
- };
- /**
- * Single upcast converter to the <sideCard/> element with all its attributes.
- */
- const upcastCard = ( viewElement, { writer } ) => {
- const sideCard = writer.createElement( 'sideCard' );
- const type = getTypeFromViewElement( viewElement );
- writer.setAttribute( 'cardType', type, sideCard );
- const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
- return child.is( 'element', 'div' ) && child.hasClass( 'side-card-url' );
- } );
- if ( urlWrapper ) {
- writer.setAttribute( 'cardURL', urlWrapper.getChild( 0 ).data, sideCard );
- }
- return sideCard;
- };
- /**
- * Helper for creating a DOM button with an editor callback.
- */
- const addActionButton = ( text, callback, domElement, editor ) => {
- const domDocument = domElement.ownerDocument;
- const button = createElement( domDocument, 'button', {}, [ text ] );
- button.addEventListener( 'click', () => {
- editor.model.change( callback );
- } );
- domElement.appendChild( button );
- return button;
- };
- /**
- * Helper function that creates the card editing UI inside the card.
- */
- const createActionsView = ( editor, modelElement ) => function( domElement ) {
- //
- // Set the URL action button.
- //
- addActionButton( 'Set URL', writer => {
- // eslint-disable-next-line no-alert
- const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
- writer.setAttribute( 'cardURL', newURL, modelElement );
- }, domElement, editor );
- const currentType = modelElement.getAttribute( 'cardType' );
- const newType = currentType === 'default' ? 'alternate' : 'default';
- //
- // Change the card action button.
- //
- addActionButton( 'Change type', writer => {
- writer.setAttribute( 'cardType', newType, modelElement );
- }, domElement, editor );
- const childCount = modelElement.childCount;
- //
- // Add the content section to the card action button.
- //
- const addButton = addActionButton( 'Add section', writer => {
- writer.insertElement( 'sideCardSection', modelElement, 'end' );
- }, domElement, editor );
- // Disable the button so only 1-3 content boxes are in the card (there will always be a title).
- if ( childCount > 4 ) {
- addButton.setAttribute( 'disabled', 'disabled' );
- }
- //
- // Remove the content section from the card action button.
- //
- const removeButton = addActionButton( 'Remove section', writer => {
- writer.remove( modelElement.getChild( childCount - 1 ) );
- }, domElement, editor );
- // Disable the button so only 1-3 content boxes are in the card (there will always be a title).
- if ( childCount < 3 ) {
- removeButton.setAttribute( 'disabled', 'disabled' );
- }
- };
- /**
- * The downcast converter for the <sideCard/> element.
- *
- * It returns the full view structure based on the current state of the model element.
- */
- const downcastSideCard = ( editor, { asWidget } ) => {
- return ( modelElement, { writer, consumable, mapper } ) => {
- const type = modelElement.getAttribute( 'cardType' ) || 'default';
- // The main view element for the side card.
- const sideCardView = writer.createContainerElement( 'aside', {
- class: `side-card side-card-${ type }`
- } );
- // Create inner views from the side card children.
- for ( const child of modelElement.getChildren() ) {
- const childView = writer.createEditableElement( 'div' );
- // Child is either a "title" or "section".
- if ( child.is( 'element', 'sideCardTitle' ) ) {
- writer.addClass( 'side-card-title', childView );
- } else {
- writer.addClass( 'side-card-section', childView );
- }
- // It is important to consume and bind converted elements.
- consumable.consume( child, 'insert' );
- mapper.bindElements( child, childView );
- // Make it an editable part of the widget.
- if ( asWidget ) {
- toWidgetEditable( childView, writer );
- }
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
- }
- const urlAttribute = modelElement.getAttribute( 'cardURL' );
- // Do not render an empty URL field.
- if ( urlAttribute ) {
- const urlBox = writer.createRawElement( 'div', {
- class: 'side-card-url'
- }, function( domElement ) {
- domElement.innerText = `URL: "${ urlAttribute }"`;
- } );
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
- }
- // Inner element used to render a simple UI that allows to change the side card's attributes.
- // It will only be needed in the editing view inside the widgetized element.
- // The data output should not contain this section.
- if ( asWidget ) {
- const actionsView = writer.createRawElement( 'div', {
- class: 'side-card-actions',
- contenteditable: 'false', // Prevents editing of the element.
- 'data-cke-ignore-events': 'true' // Allows using custom UI elements inside the editing view.
- }, createActionsView( editor, modelElement ) ); // See the full code for details.
- writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
- toWidget( sideCardView, writer, { widgetLabel: 'Side card', hasSelectionHandle: true } );
- }
- return sideCardView;
- };
- };
- class InsertCardCommand extends Command {
- /**
- * Refresh used schema definition to check if a side card can be inserted in the current selection.
- */
- refresh() {
- const model = this.editor.model;
- const validParent = findOptimalInsertionPosition( model.document.selection, model );
- this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
- }
- /**
- * Creates a full side card element with all required children and attributes.
- */
- execute() {
- const model = this.editor.model;
- const selection = model.document.selection;
- const insertPosition = findOptimalInsertionPosition( selection, model );
- model.change( writer => {
- const sideCard = writer.createElement( 'sideCard', { cardType: 'default' } );
- const title = writer.createElement( 'sideCardTitle' );
- const section = writer.createElement( 'sideCardSection' );
- const paragraph = writer.createElement( 'paragraph' );
- writer.insert( title, sideCard, 0 );
- writer.insert( section, sideCard, 1 );
- writer.insert( paragraph, section, 0 );
- model.insertContent( sideCard, insertPosition );
- writer.setSelection( writer.createPositionAt( title, 0 ) );
- } );
- }
- }
- class ComplexBox extends Plugin {
- constructor( editor ) {
- super( editor );
- this._defineSchema();
- this._defineConversion();
- editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
- this._defineUI();
- }
- _defineConversion() {
- const editor = this.editor;
- const conversion = editor.conversion;
- conversion.for( 'upcast' )
- .elementToElement( {
- view: { name: 'aside', classes: [ 'side-card' ] },
- model: upcastCard
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-title' ] },
- model: 'sideCardTitle'
- } )
- .elementToElement( {
- view: { name: 'div', classes: [ 'side-card-section' ] },
- model: 'sideCardSection'
- } );
- // The downcast conversion must be split as you need a widget in the editing pipeline.
- conversion.for( 'editingDowncast' ).elementToElement( {
- model: 'sideCard',
- view: downcastSideCard( editor, { asWidget: true } ),
- triggerBy: {
- attributes: [ 'cardType', 'cardURL' ],
- children: [ 'sideCardSection' ]
- }
- } );
- // The data downcast is always executed from the current model stat, so `triggerBy` will take no effect.
- conversion.for( 'dataDowncast' ).elementToElement( {
- model: 'sideCard',
- view: downcastSideCard( editor, { asWidget: false } )
- } );
- }
- _defineSchema() {
- const schema = this.editor.model.schema;
- // The main element with attributes for type and URL:
- schema.register( 'sideCard', {
- allowWhere: '$block',
- isObject: true,
- allowAttributes: [ 'cardType', 'cardURL' ]
- } );
- // Disallow side card nesting.
- schema.addChildCheck( ( context, childDefinition ) => {
- if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
- return false;
- }
- } );
- // A text-only title.
- schema.register( 'sideCardTitle', {
- isLimit: true,
- allowIn: 'sideCard'
- } );
- // Allow text in title...
- schema.extend( '$text', { allowIn: 'sideCardTitle' } );
- // ...but disallow any text attribute inside.
- schema.addAttributeCheck( context => {
- if ( context.endsWith( 'sideCardTitle $text' ) ) {
- return false;
- }
- } );
- // A content block which can have any content allowed in $root.
- schema.register( 'sideCardSection', {
- isLimit: true,
- allowIn: 'sideCard',
- allowContentOf: '$root'
- } );
- }
- _defineUI() {
- const editor = this.editor;
- // Defines a simple text button.
- editor.ui.componentFactory.add( 'insertCard', locale => {
- const button = new ButtonView( locale );
- const command = editor.commands.get( 'insertCard' );
- button.set( {
- withText: true,
- icon: false,
- label: 'Insert card'
- } );
- button.bind( 'isEnabled' ).to( command );
- button.on( 'execute', () => {
- editor.execute( 'insertCard' );
- editor.editing.view.focus();
- } );
- return button;
- } );
- }
- }
- ClassicEditor
- .create( document.querySelector( '#editor-element-reconversion' ), {
- cloudServices: CS_CONFIG,
- extraPlugins: [ ComplexBox ],
- image: {
- toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
- },
- table: {
- contentToolbar: [
- 'tableColumn',
- 'tableRow',
- 'mergeTableCells'
- ]
- },
- toolbar: {
- items: [ 'heading', '|', 'bold', 'italic', '|', 'insertCard' ],
- viewportTopOffset: window.getViewportTopOffsetConfig()
- }
- } )
- .then( editor => {
- window.editor = editor;
- } )
- .catch( err => {
- console.error( err );
- } );
|