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;
}