/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals console, window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import List from '@ckeditor/ckeditor5-list/src/list'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget/src/utils'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import Command from '@ckeditor/ckeditor5-core/src/command'; import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; import Model from '@ckeditor/ckeditor5-ui/src/model'; class PlaceholderCommand extends Command { execute( { value } ) { const editor = this.editor; editor.model.change( writer => { // Create a elment with the "name" attribute... const placeholder = writer.createElement( 'placeholder', { name: value } ); // ... and insert it into the document. editor.model.insertContent( placeholder ); // Put the selection on the inserted element. writer.setSelection( placeholder, 'on' ); } ); } refresh() { const model = this.editor.model; const selection = model.document.selection; const isAllowed = model.schema.checkChild( selection.focus.parent, 'placeholder' ); this.isEnabled = isAllowed; } } class Placeholder extends Plugin { static get requires() { return [ PlaceholderEditing, PlaceholderUI ]; } } class PlaceholderUI extends Plugin { init() { const editor = this.editor; const t = editor.t; const placeholderNames = editor.config.get( 'placeholderConfig.types' ); // The "placeholder" dropdown must be registered among the UI components of the editor // to be displayed in the toolbar. editor.ui.componentFactory.add( 'placeholder', locale => { const dropdownView = createDropdown( locale ); // Populate the list in the dropdown with items. addListToDropdown( dropdownView, getDropdownItemsDefinitions( placeholderNames ) ); dropdownView.buttonView.set( { // The t() function helps localize the editor. All strings enclosed in t() can be // translated and change when the language of the editor changes. label: t( 'Placeholder' ), tooltip: true, withText: true } ); // Disable the placeholder button when the command is disabled. const command = editor.commands.get( 'placeholder' ); dropdownView.bind( 'isEnabled' ).to( command ); // Execute the command when the dropdown item is clicked (executed). this.listenTo( dropdownView, 'execute', evt => { editor.execute( 'placeholder', { value: evt.source.commandParam } ); editor.editing.view.focus(); } ); return dropdownView; } ); } } function getDropdownItemsDefinitions( placeholderNames ) { const itemDefinitions = new Collection(); for ( const name of placeholderNames ) { const definition = { type: 'button', model: new Model( { commandParam: name, label: name, withText: true } ) }; // Add the item definition to the collection. itemDefinitions.add( definition ); } return itemDefinitions; } class PlaceholderEditing extends Plugin { static get requires() { return [ Widget ]; } init() { console.log( 'PlaceholderEditing#init() got called' ); this._defineSchema(); this._defineConverters(); this.editor.commands.add( 'placeholder', new PlaceholderCommand( this.editor ) ); this.editor.editing.mapper.on( 'viewToModelPosition', viewToModelPositionOutsideModelElement( this.editor.model, viewElement => viewElement.hasClass( 'placeholder' ) ) ); this.editor.config.define( 'placeholderConfig', { types: [ 'date', 'first name', 'surname' ] } ); } _defineSchema() { const schema = this.editor.model.schema; schema.register( 'placeholder', { // Allow wherever text is allowed: allowWhere: '$text', // The placeholder will act as an inline node: isInline: true, // The inline widget is self-contained so it cannot be split by the caret and it can be selected: isObject: true, // The placeholder can have many types, like date, name, surname, etc: allowAttributes: [ 'name' ] } ); } _defineConverters() { const conversion = this.editor.conversion; conversion.for( 'upcast' ).elementToElement( { view: { name: 'span', classes: [ 'placeholder' ] }, model: ( viewElement, modelWriter ) => { // Extract the "name" from "{name}". const name = viewElement.getChild( 0 ).data.slice( 1, -1 ); return modelWriter.createElement( 'placeholder', { name } ); } } ); conversion.for( 'editingDowncast' ).elementToElement( { model: 'placeholder', view: ( modelItem, viewWriter ) => { const widgetElement = createPlaceholderView( modelItem, viewWriter ); // Enable widget handling on a placeholder element inside the editing view. return toWidget( widgetElement, viewWriter ); } } ); conversion.for( 'dataDowncast' ).elementToElement( { model: 'placeholder', view: createPlaceholderView } ); // Helper method for both downcast converters. function createPlaceholderView( modelItem, viewWriter ) { const name = modelItem.getAttribute( 'name' ); const placeholderView = viewWriter.createContainerElement( 'span', { class: 'placeholder' } ); // Insert the placeholder name (as a text). const innerText = viewWriter.createText( '{' + name + '}' ); viewWriter.insert( viewWriter.createPositionAt( placeholderView, 0 ), innerText ); return placeholderView; } } } ClassicEditor .create( document.querySelector( '#snippet-inline-widget' ), { plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, Placeholder ], toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'placeholder' ], placeholderConfig: { types: [ 'date', 'color', 'first name', 'surname' ] } } ) .then( editor => { console.log( 'Editor was initialized', editor ); // Expose for playing in the console. window.editor = editor; } ) .catch( error => { console.error( error.stack ); } );