From eba89cee622b309389cd564c7ea7c89dc505afa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 29 Jul 2022 01:03:17 +0200 Subject: [PATCH] Started to rewrite CKEDITOR placeholder plugin for CKEDITOR5. --- assets/ckeditor/html_label.js | 6 +- .../plugins/PartDBLabel/PartDBLabel.css | 22 +++ .../plugins/PartDBLabel/PartDBLabel.js | 16 ++ .../plugins/PartDBLabel/PartDBLabelCommand.js | 31 ++++ .../plugins/PartDBLabel/PartDBLabelEditing.js | 86 ++++++++++ .../plugins/PartDBLabel/PartDBLabelUI.js | 149 ++++++++++++++++++ .../ckeditor/plugins/PartDBLabel/lang/en.js | 58 +++++++ src/Form/Type/RichTextEditorType.php | 7 + 8 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 assets/ckeditor/plugins/PartDBLabel/PartDBLabel.css create mode 100644 assets/ckeditor/plugins/PartDBLabel/PartDBLabel.js create mode 100644 assets/ckeditor/plugins/PartDBLabel/PartDBLabelCommand.js create mode 100644 assets/ckeditor/plugins/PartDBLabel/PartDBLabelEditing.js create mode 100644 assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js create mode 100644 assets/ckeditor/plugins/PartDBLabel/lang/en.js diff --git a/assets/ckeditor/html_label.js b/assets/ckeditor/html_label.js index 0110a344..fa7966b6 100644 --- a/assets/ckeditor/html_label.js +++ b/assets/ckeditor/html_label.js @@ -60,6 +60,7 @@ import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js'; import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js'; import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js'; +import PartDBLabel from "./plugins/PartDBLabel/PartDBLabel"; class Editor extends ClassicEditor {} @@ -119,13 +120,16 @@ Editor.builtinPlugins = [ TableProperties, TableToolbar, Underline, - WordCount + WordCount, + + PartDBLabel ]; // Editor configuration. Editor.defaultConfig = { toolbar: { items: [ + 'partdb_label', 'heading', 'alignment', '|', diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.css b/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.css new file mode 100644 index 00000000..9171c357 --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.css @@ -0,0 +1,22 @@ +.cke_placeholder { + background: #ffff00; + padding: 4px 2px; + outline-offset: -2px; + line-height: 1em; + margin: 0 1px; +} + +.cke_placeholder::selection { + display: none; +} + +/*.cke_overflow_dropdown { + overflow: auto; + height: 40vh; +}*/ + +/** Make long editor dropdown panels scrollable */ +.ck-dropdown__panel { + overflow: auto; + max-height: 40vh; +} diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.js new file mode 100644 index 00000000..44f4a37e --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabel.js @@ -0,0 +1,16 @@ +import PartDBLabelUI from "./PartDBLabelUI"; +import PartDBLabelEditing from "./PartDBLabelEditing"; + +import "./PartDBLabel.css"; + +import Plugin from "@ckeditor/ckeditor5-core/src/plugin"; + +export default class PartDBLabel extends Plugin { + static get requires() { + return [PartDBLabelUI, PartDBLabelEditing]; + } + + static get pluginName() { + return 'PartDBLabel'; + } +} \ No newline at end of file diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelCommand.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelCommand.js new file mode 100644 index 00000000..4fa30345 --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelCommand.js @@ -0,0 +1,31 @@ +import Command from '@ckeditor/ckeditor5-core/src/command'; + +export default class PartDBLabelCommand extends Command { + execute( { value } ) { + const editor = this.editor; + const selection = editor.model.document.selection; + + editor.model.change( writer => { + // Create a elment with the "name" attribute (and all the selection attributes)... + const placeholder = writer.createElement( 'partdb_label', { + ...Object.fromEntries( selection.getAttributes() ), + 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, 'partdb_label' ); + + this.isEnabled = isAllowed; + } +} \ No newline at end of file diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelEditing.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelEditing.js new file mode 100644 index 00000000..dfe3879d --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelEditing.js @@ -0,0 +1,86 @@ +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import PartDBLabelCommand from "./PartDBLabelCommand"; + +import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; + +export default class PartDBLabelEditing extends Plugin { + static get requires() { // ADDED + return [ Widget ]; + } + + init() { + + this._defineSchema(); + this._defineConverters(); + + this.editor.commands.add('partdb_label', new PartDBLabelCommand( this.editor ) ); + } + + _defineSchema() { + const schema = this.editor.model.schema; + + schema.register('partdb_label', { + // 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 can be selected: + isObject: true, + + allowAttributesOf: '$text', + + allowAttributes: [ 'name' ] + }); + } + + _defineConverters() { + const conversion = this.editor.conversion; + + conversion.for( 'upcast' ).elementToElement( { + view: { + name: 'span', + classes: [ 'cke_placeholder' ] + }, + model: ( viewElement, { writer: modelWriter } ) => { + // Extract the "name" from "{name}". + const name = viewElement.getChild( 0 ).data.slice( 1, -1 ); + + return modelWriter.createElement( 'label-placeholder', { name } ); + } + } ); + + conversion.for( 'editingDowncast' ).elementToElement( { + model: 'partdb_label', + view: ( modelItem, { writer: 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: 'partdb_label', + view: ( modelItem, { writer: viewWriter } ) => createPlaceholderView( modelItem, viewWriter ) + } ); + + // Helper method for both downcast converters. + function createPlaceholderView( modelItem, viewWriter ) { + const name = modelItem.getAttribute( 'name' ); + + const placeholderView = viewWriter.createContainerElement( 'span', { + class: 'cke_placeholder' + } ); + + // Insert the placeholder name (as a text). + const innerText = viewWriter.createText( name ); + viewWriter.insert( viewWriter.createPositionAt( placeholderView, 0 ), innerText ); + + return placeholderView; + } + } + +} \ No newline at end of file diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js new file mode 100644 index 00000000..58e0df1e --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js @@ -0,0 +1,149 @@ +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +require('./lang/en.js'); + +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'; + +export default class PartDBLabelUI extends Plugin { + init() { + const editor = this.editor; + const t = editor.t; + + // The "placeholder" dropdown must be registered among the UI components of the editor + // to be displayed in the toolbar. + editor.ui.componentFactory.add( 'partdb_label', locale => { + const dropdownView = createDropdown( locale ); + + // Populate the list in the dropdown with items. + addListToDropdown( dropdownView, getDropdownItemsDefinitions(t) ); + + 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( 'part_db.label' ), + tooltip: true, + withText: true + } ); + + // Disable the placeholder button when the command is disabled. + const command = editor.commands.get( 'partdb_label' ); + dropdownView.bind( 'isEnabled' ).to( command ); + + // Execute the command when the dropdown item is clicked (executed). + this.listenTo( dropdownView, 'execute', evt => { + editor.execute( 'partdb_label', { value: evt.source.commandParam } ); + editor.editing.view.focus(); + } ); + + return dropdownView; + } ); + } +} + +const PLACEHOLDERS = [ + { + label: 'section.part', + entries: [ + ['[[ID]]', 'part.id'], + ['[[NAME]]', 'part.name'], + ['[[CATEGORY]]', 'part.category'], + ['[[CATEGORY_FULL]]', 'part.category_full'], + ['[[MANUFACTURER]]', 'part.manufacturer'], + ['[[MANUFACTURER_FULL]]', 'part.manufacturer_full'], + ['[[FOOTPRINT]]', 'part.footprint'], + ['[[FOOTPRINT_FULL]]', 'part.footprint'], + ['[[MASS]]', 'part.mass'], + ['[[MPN]]', 'part.mpn'], + ['[[TAGS]]', 'part.tags'], + ['[[M_STATUS]]', 'part.status'], + ['[[DESCRIPTION]]', 'part.description'], + ['[[DESCRIPTION_T]]', 'part.description_t'], + ['[[COMMENT]]', 'part.comment'], + ['[[COMMENT_T]]', 'part.comment_t'], + ['[[LAST_MODIFIED]]', 'part.last_modified'], + ['[[CREATION_DATE]]', 'part.creation_date'], + ] + }, + { + label: 'section.part_lot', + entries: [ + ['[[LOT_ID]]', 'lot.id'], + ['[[LOT_NAME]]', 'lot.name'], + ['[[LOT_COMMENT]]', 'lot.comment'], + ['[[EXPIRATION_DATE]]', 'lot.expiration_date'], + ['[[AMOUNT]]', 'lot.amount'], + ['[[LOCATION]]', 'lot.location'], + ['[[LOCATION_FULL]]', 'lot.location_full'], + ] + }, + { + label: 'section.storelocation', + entries: [ + ['[[ID]]', 'storelocation.id'], + ['[[NAME]]', 'storelocation.name'], + ['[[FULL_PATH]]', 'storelocation.full_path'], + ['[[PARENT]]', 'storelocation.parent_name'], + ['[[PARENT_FULL_PATH]]', 'storelocation.parent_full_path'], + ['[[COMMENT]]', 'storelocation.comment'], + ['[[COMMENT_T]]', 'storelocation.comment_t'], + ['[[LAST_MODIFIED]]', 'storelocation.last_modified'], + ['[[CREATION_DATE]]', 'storelocation.creation_date'], + ] + }, + { + label: 'section.global', + entries: [ + ['[[USERNAME]]', 'global.username'], + ['[[USERNAME_FULL]]', 'global.username_full'], + ['[[DATETIME]]', 'global.datetime'], + ['[[DATE]]', 'global.date'], + ['[[TIME]]', 'global.time'], + ['[[INSTALL_NAME]]', 'global.install_name'], + ['[[TYPE]]', 'global.type'] + ], + }, +]; + + +function getDropdownItemsDefinitions(t) { + const itemDefinitions = new Collection(); + + for ( const group of PLACEHOLDERS) { + //Add group header + itemDefinitions.add({ + 'type': 'separator', + model: new Model( { + withText: true, + }) + }); + + itemDefinitions.add({ + type: 'button', + model: new Model( { + label: t(group.label), + withText: true, + isEnabled: false, + } ) + }); + + //Add group entries + for ( const entry of group.entries) { + const definition = { + type: 'button', + model: new Model( { + commandParam: entry[0], + label: t(entry[1]), + withText: true + } ), + }; + + // Add the item definition to the collection. + itemDefinitions.add( definition ); + } + } + + return itemDefinitions; +} \ No newline at end of file diff --git a/assets/ckeditor/plugins/PartDBLabel/lang/en.js b/assets/ckeditor/plugins/PartDBLabel/lang/en.js new file mode 100644 index 00000000..89a0291c --- /dev/null +++ b/assets/ckeditor/plugins/PartDBLabel/lang/en.js @@ -0,0 +1,58 @@ +// Make sure that the global object is defined. If not, define it. +window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {}; + +// Make sure that the dictionary for Polish translations exist. +window.CKEDITOR_TRANSLATIONS[ 'en' ] = window.CKEDITOR_TRANSLATIONS[ 'en' ] || {}; +window.CKEDITOR_TRANSLATIONS[ 'en' ].dictionary = window.CKEDITOR_TRANSLATIONS[ 'en' ].dictionary || {}; + +// Extend the dictionary for Polish translations with your translations: +Object.assign( window.CKEDITOR_TRANSLATIONS[ 'en' ].dictionary, { + 'part_db.title': 'Insert Placeholders', + 'part_db.label': 'Placeholders', + 'section.global': 'Globals', + 'section.part': 'Part', + 'section.part_lot': 'Part lot', + 'section.storelocation': 'Storage location', + 'part.id': 'Database ID', + 'part.name': 'Part name', + 'part.category': 'Category', + 'part.category_full': 'Category (Full path)', + 'part.manufacturer': 'Manufacturer', + 'part.manufacturer_full': 'Manufacturer (Full path)', + 'part.footprint': 'Footprint', + 'part.footprint_full': 'Footprint (Full path)', + 'part.mass': 'Mass', + 'part.tags': 'Tags', + 'part.mpn': 'Manufacturer Product Number (MPN)', + 'part.status': 'Manufacturing status', + 'part.description': 'Description', + 'part.description_t': 'Description (Text)', + 'part.comment': 'Comment', + 'part.comment_t': 'Comment (Text)', + 'part.last_modified': 'Last modified datetime', + 'part.creation_date': 'Creation datetime', + 'global.username': 'Username', + 'global.username_full': 'Username (including name)', + 'global.datetime': 'Current datetime', + 'global.date': 'Current date', + 'global.time': 'Current time', + 'global.install_name': 'Instance name', + 'global.type': 'Target type', + 'lot.id': 'Lot ID', + 'lot.name': 'Lot name', + 'lot.comment': 'Lot comment', + 'lot.expiration_date': 'Expiration date', + 'lot.amount': 'Lot amount', + 'lot.location': 'Storage location', + 'lot.location_full': 'Storage location (Full path)', + + 'storelocation.id': 'Location ID', + 'storelocation.name': 'Name', + 'storelocation.full_path': 'Full path', + 'storelocation.parent_name': 'Parent name', + 'storelocation.parent_full_path': 'Parent full path', + 'storelocation.comment': 'Comment', + 'storelocation.comment_t': 'Comment (Text)', + 'storelocation.last_modified': 'Last modified datetime', + 'storelocation.creation_date': 'Createion datetime', +} ); \ No newline at end of file diff --git a/src/Form/Type/RichTextEditorType.php b/src/Form/Type/RichTextEditorType.php index 4ed31aee..3424b8a0 100644 --- a/src/Form/Type/RichTextEditorType.php +++ b/src/Form/Type/RichTextEditorType.php @@ -16,6 +16,9 @@ class RichTextEditorType extends AbstractType $resolver->setDefault('mode', 'markdown-full'); $resolver->setAllowedValues('mode', ['html-label', 'markdown-single_line', 'markdown-full']); + + $resolver->setDefault('required', false); + } public function getBlockPrefix() @@ -34,11 +37,15 @@ class RichTextEditorType extends AbstractType { $tmp = []; + //Set novalidate attribute or we will get problems that form can not be submitted as textarea is not focusable + $tmp['novalidate'] = 'novalidate'; + $tmp['data-mode'] = $options['mode']; //Add our data-controller element to the textarea $tmp['data-controller'] = 'elements--ckeditor'; + return $tmp; }