From 058d349726399ad07b3cd448f0bca6affd6ac972 Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Wed, 14 Feb 2024 09:23:08 +0100 Subject: [PATCH] IBX-6932: Fixed adding custom attrs to list (#139) * IBX-6932: Fixed adding custom attrs to list * Corrected problem with listItem, Added remove custom classes and attributes on new item list * Corrected clean custom classes * fix --------- Co-authored-by: matx132 --- .../custom-attributes-command.js | 18 ++- .../custom-attributes-editing.js | 136 +++++++++++++++++- .../custom-attributes/custom-attributes-ui.js | 51 +++++-- .../js/CKEditor/plugins/elements-path.js | 47 ++++++ .../Resources/public/scss/_elements-path.scss | 8 +- .../Resources/translations/ck_editor.en.xliff | 5 + 6 files changed, 246 insertions(+), 19 deletions(-) diff --git a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-command.js b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-command.js index d3f6fc9b..9a2eea40 100644 --- a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-command.js +++ b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-command.js @@ -44,10 +44,24 @@ class IbexaCustomAttributesCommand extends Command { refresh() { const { selection } = this.editor.model.document; const parentElement = selection.getSelectedElement() ?? selection.getFirstPosition().parent; + let parentElementName = parentElement.name; + + if (this.editor.isListSelected) { + const mapping = { + bulleted: 'ul', + numbered: 'ol', + }; + const listType = parentElement.getAttribute('listType'); + + if (mapping[listType]) { + parentElementName = mapping[listType]; + } + } + const customAttributesConfig = getCustomAttributesConfig(); const customClassesConfig = getCustomClassesConfig(); - const parentElementAttributesConfig = customAttributesConfig[parentElement.name]; - const parentElementClassesConfig = customClassesConfig[parentElement.name]; + const parentElementAttributesConfig = customAttributesConfig[parentElementName]; + const parentElementClassesConfig = customClassesConfig[parentElementName]; const isEnabled = parentElementAttributesConfig || parentElementClassesConfig; this.isEnabled = !!isEnabled; diff --git a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-editing.js b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-editing.js index 3bb3fd6e..059a22c5 100644 --- a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-editing.js +++ b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-editing.js @@ -22,8 +22,48 @@ class IbexaCustomAttributesEditing extends Plugin { }, }); - Object.values(customAttributesConfig).forEach((customAttributes) => { + Object.entries(customAttributesConfig).forEach(([element, customAttributes]) => { + const isList = element === 'ul' || element === 'ol'; + Object.keys(customAttributes).forEach((customAttributeName) => { + if (isList) { + this.editor.conversion.for('dataDowncast').add((dispatcher) => { + dispatcher.on(`attribute:list-${customAttributeName}:listItem`, (event, data, conversionApi) => { + const viewItem = conversionApi.mapper.toViewElement(data.item); + + conversionApi.writer.setAttribute( + `data-ezattribute-${customAttributeName}`, + data.attributeNewValue, + viewItem.parent, + ); + }); + }); + + this.editor.conversion.for('editingDowncast').add((dispatcher) => { + dispatcher.on(`attribute:list-${customAttributeName}:listItem`, (event, data, conversionApi) => { + const viewItem = conversionApi.mapper.toViewElement(data.item); + + conversionApi.writer.setAttribute( + `data-ezattribute-${customAttributeName}`, + data.attributeNewValue, + viewItem.parent, + ); + }); + }); + + this.editor.conversion.for('upcast').add((dispatcher) => { + dispatcher.on('element:li', (event, data, conversionApi) => { + const listParent = data.viewItem.parent; + const listItem = data.modelRange.start.nodeAfter || data.modelRange.end.nodeBefore; + const attributeValue = listParent.getAttribute(`data-ezattribute-${customAttributeName}`); + + conversionApi.writer.setAttribute(`list-${customAttributeName}`, attributeValue, listItem); + }); + }); + + return; + } + conversion.attributeToAttribute({ model: { key: customAttributeName, @@ -34,6 +74,55 @@ class IbexaCustomAttributesEditing extends Plugin { }); }); }); + + this.editor.conversion.for('dataDowncast').add((dispatcher) => { + dispatcher.on('attribute:list-custom-classes:listItem', (event, data, conversionApi) => { + if (data.attributeKey !== 'list-custom-classes' || data.attributeNewValue === '') { + return; + } + + const viewItem = conversionApi.mapper.toViewElement(data.item); + const previousElement = viewItem.parent.previousSibling; + + conversionApi.writer.setAttribute('class', data.attributeNewValue, viewItem.parent); + + if (previousElement?.name === viewItem.parent.name) { + conversionApi.writer.mergeContainers(conversionApi.writer.createPositionAfter(previousElement)); + } + }); + }); + + this.editor.conversion.for('editingDowncast').add((dispatcher) => { + dispatcher.on('attribute:list-custom-classes:listItem', (event, data, conversionApi) => { + if (data.attributeKey !== 'list-custom-classes' || data.attributeNewValue === '') { + return; + } + + const viewItem = conversionApi.mapper.toViewElement(data.item); + const previousElement = viewItem.parent.previousSibling; + const nextElement = viewItem.parent.nextSibling; + + conversionApi.writer.setAttribute('class', data.attributeNewValue, viewItem.parent); + + if (previousElement?.name === viewItem.parent.name) { + conversionApi.writer.mergeContainers(conversionApi.writer.createPositionAfter(previousElement)); + } + + if (nextElement?.name === viewItem.parent.name) { + conversionApi.writer.mergeContainers(conversionApi.writer.createPositionBefore(nextElement)); + } + }); + }); + + this.editor.conversion.for('upcast').add((dispatcher) => { + dispatcher.on('element:li', (event, data, conversionApi) => { + const listParent = data.viewItem.parent; + const listItem = data.modelRange.start.nodeAfter || data.modelRange.end.nodeBefore; + const classes = listParent.getAttribute('class'); + + conversionApi.writer.setAttribute('list-custom-classes', classes, listItem); + }); + }); } extendSchema(schema, element, definition) { @@ -44,26 +133,63 @@ class IbexaCustomAttributesEditing extends Plugin { } } - init() { + cleanAttributes(element, customs) { const { model } = this.editor; + + Object.entries(customs).forEach(([elementName, config]) => { + if (elementName === element.name) { + return; + } + + model.change((writer) => { + Object.keys(config).forEach((name) => { + writer.removeAttribute(name, element); + }); + }); + }); + } + + init() { + const { commands, model } = this.editor; const customAttributesConfig = getCustomAttributesConfig(); const customClassesConfig = getCustomClassesConfig(); const elementsWithCustomAttributes = Object.keys(customAttributesConfig); const elementsWithCustomClasses = Object.keys(customClassesConfig); elementsWithCustomAttributes.forEach((element) => { + const isList = element === 'ul' || element === 'ol'; + const prefix = isList ? 'list-' : ''; + const elementName = isList ? 'listItem' : element; const customAttributes = Object.keys(customAttributesConfig[element]); - this.extendSchema(model.schema, element, { allowAttributes: customAttributes }); + customAttributes.forEach((customAttribute) => { + this.extendSchema(model.schema, elementName, { allowAttributes: `${prefix}${customAttribute}` }); + }); }); elementsWithCustomClasses.forEach((element) => { - this.extendSchema(model.schema, element, { allowAttributes: 'custom-classes' }); + const isList = element === 'ul' || element === 'ol'; + const prefix = isList ? 'list-' : ''; + const elementName = isList ? 'listItem' : element; + + this.extendSchema(model.schema, elementName, { allowAttributes: `${prefix}custom-classes` }); }); this.defineConverters(); - this.editor.commands.add('insertIbexaCustomAttributes', new IbexaCustomAttributesCommand(this.editor)); + commands.get('enter').on('afterExecute', () => { + const blocks = model.document.selection.getSelectedBlocks(); + + for (const block of blocks) { + this.cleanAttributes(block, customAttributesConfig); + + model.change((writer) => { + writer.removeAttribute('custom-classes', block); + }); + } + }); + + commands.add('insertIbexaCustomAttributes', new IbexaCustomAttributesCommand(this.editor)); } } diff --git a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-ui.js b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-ui.js index 262ee9be..a1c83670 100644 --- a/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-ui.js +++ b/src/bundle/Resources/public/js/CKEditor/custom-attributes/custom-attributes-ui.js @@ -22,16 +22,26 @@ class IbexaAttributesUI extends Plugin { return this.editor.model.document.selection.getSelectedElement() || this.editor.model.document.selection.anchor.parent; } + getAttributePrefix() { + return this.editor.isListSelected ? 'list-' : ''; + } + createFormView() { const formView = new IbexaCustomAttributesFormView({ locale: this.editor.locale }); this.listenTo(formView, 'save-custom-attributes', () => { const values = this.formView.getValues(); - const modelElement = this.getModelElement(); + const modelElements = this.editor.isListSelected + ? Array.from(this.editor.model.document.selection.getSelectedBlocks()) + : [this.getModelElement()]; this.editor.model.change((writer) => { Object.entries(values).forEach(([name, value]) => { - writer.setAttribute(name, value, modelElement); + const prefix = this.getAttributePrefix(); + + modelElements.forEach((modelElement) => { + writer.setAttribute(`${prefix}${name}`, value, modelElement); + }); }); }); @@ -40,11 +50,17 @@ class IbexaAttributesUI extends Plugin { this.listenTo(formView, 'remove-custom-attributes', () => { const values = this.formView.getValues(); - const modelElement = this.getModelElement(); + const modelElements = this.editor.isListSelected + ? Array.from(this.editor.model.document.selection.getSelectedBlocks()) + : [this.getModelElement()]; this.editor.model.change((writer) => { Object.keys(values).forEach((name) => { - writer.removeAttribute(name, modelElement); + const prefix = this.getAttributePrefix(); + + modelElements.forEach((modelElement) => { + writer.removeAttribute(`${prefix}${name}`, modelElement); + }); }); }); @@ -67,18 +83,33 @@ class IbexaAttributesUI extends Plugin { const parentElement = this.getModelElement(); const customAttributesConfig = getCustomAttributesConfig(); const customClassesConfig = getCustomClassesConfig(); - const customAttributes = customAttributesConfig[parentElement.name] ?? {}; - const customClasses = customClassesConfig[parentElement.name]; + const prefix = this.getAttributePrefix(); + let parentElementName = parentElement.name; + + if (this.editor.isListSelected) { + const mapping = { + bulleted: 'ul', + numbered: 'ol', + }; + const listType = parentElement.getAttribute('listType'); + + if (mapping[listType]) { + parentElementName = mapping[listType]; + } + } + + const customAttributes = customAttributesConfig[parentElementName] ?? {}; + const customClasses = customClassesConfig[parentElementName]; const areCustomAttributesSet = - parentElement.hasAttribute('custom-classes') || - Object.keys(customAttributes).some((customAttributeName) => parentElement.hasAttribute(customAttributeName)); + parentElement.hasAttribute(`${prefix}custom-classes`) || + Object.keys(customAttributes).some((customAttributeName) => parentElement.hasAttribute(`${prefix}${customAttributeName}`)); const attributesValues = Object.entries(customAttributes).reduce((output, [name, config]) => { - output[name] = areCustomAttributesSet ? parentElement.getAttribute(name) : config.defaultValue; + output[name] = areCustomAttributesSet ? parentElement.getAttribute(`${prefix}${name}`) : config.defaultValue; return output; }, {}); const defaultCustomClasses = customClasses?.defaultValue ?? ''; - const classesValue = areCustomAttributesSet ? parentElement.getAttribute('custom-classes') : defaultCustomClasses; + const classesValue = areCustomAttributesSet ? parentElement.getAttribute(`${prefix}custom-classes`) : defaultCustomClasses; this.formView.destroy(); this.formView = this.createFormView(); diff --git a/src/bundle/Resources/public/js/CKEditor/plugins/elements-path.js b/src/bundle/Resources/public/js/CKEditor/plugins/elements-path.js index d3a858b7..40752e4e 100644 --- a/src/bundle/Resources/public/js/CKEditor/plugins/elements-path.js +++ b/src/bundle/Resources/public/js/CKEditor/plugins/elements-path.js @@ -1,5 +1,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +const { Translator } = window; class IbexaElementsPath extends Plugin { constructor(props) { super(props); @@ -9,11 +10,57 @@ class IbexaElementsPath extends Plugin { this.updatePath = this.updatePath.bind(this); } + addListItem(element) { + const label = Translator.trans(/*@Desc("list")*/ 'elements_path.list.label', {}, 'ck_editor'); + const pathItem = `
  • ${label}
  • `; + const container = document.createElement('ul'); + + container.insertAdjacentHTML('beforeend', pathItem); + + const listItemNode = container.querySelector('li'); + + listItemNode.addEventListener( + 'click', + () => { + let firstElement = element; + let lastElement = element; + + while (firstElement?.previousSibling?.name === 'listItem') { + firstElement = firstElement.previousSibling; + } + + while (lastElement?.nextSibling?.name === 'listItem') { + lastElement = lastElement.nextSibling; + } + + const range = this.editor.model.createRange( + this.editor.model.createPositionBefore(firstElement), + this.editor.model.createPositionAfter(lastElement), + ); + + this.editor.isListSelected = true; + this.editor.model.change((writer) => writer.setSelection(range)); + this.editor.focus(); + + this.editor.model.document.selection.once('change', () => { + this.editor.isListSelected = false; + }); + }, + false, + ); + + this.elementsPathWrapper.append(listItemNode); + } + updatePath(element) { if (element.name === '$root') { return; } + if (element.name === 'listItem') { + this.addListItem(element); + } + const pathItem = `
  • ${element.name}
  • `; const container = document.createElement('ul'); diff --git a/src/bundle/Resources/public/scss/_elements-path.scss b/src/bundle/Resources/public/scss/_elements-path.scss index 09e66a55..cef6b333 100644 --- a/src/bundle/Resources/public/scss/_elements-path.scss +++ b/src/bundle/Resources/public/scss/_elements-path.scss @@ -6,7 +6,11 @@ font-weight: bold; flex-wrap: wrap; - &__item:not(:last-child) { - margin-right: calculateRem(16px); + &__item { + cursor: pointer; + + &:not(:last-child) { + margin-right: calculateRem(16px); + } } } diff --git a/src/bundle/Resources/translations/ck_editor.en.xliff b/src/bundle/Resources/translations/ck_editor.en.xliff index ad7b1dfe..ffc67bf3 100644 --- a/src/bundle/Resources/translations/ck_editor.en.xliff +++ b/src/bundle/Resources/translations/ck_editor.en.xliff @@ -46,6 +46,11 @@ Custom styles key: custom_styles_btn.label + + list + list + key: elements_path.list.label + Embed Embed