UNPKG

@ckeditor/ckeditor5-html-support

Version:

HTML Support feature for CKEditor 5.

204 lines (203 loc) • 9.13 kB
/** * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module html-support/integrations/documentlist */ import { isEqual } from 'lodash-es'; import { Plugin } from 'ckeditor5/src/core'; import { getHtmlAttributeName, setViewAttributes } from '../utils'; import DataFilter from '../datafilter'; /** * Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature. */ export default class DocumentListElementSupport extends Plugin { /** * @inheritDoc */ static get requires() { return [DataFilter]; } /** * @inheritDoc */ static get pluginName() { return 'DocumentListElementSupport'; } /** * @inheritDoc */ init() { const editor = this.editor; if (!editor.plugins.has('DocumentListEditing')) { return; } const schema = editor.model.schema; const conversion = editor.conversion; const dataFilter = editor.plugins.get(DataFilter); const documentListEditing = editor.plugins.get('DocumentListEditing'); const viewElements = ['ul', 'ol', 'li']; // Register downcast strategy. // Note that this must be done before document list editing registers conversion in afterInit. documentListEditing.registerDowncastStrategy({ scope: 'item', attributeName: 'htmlLiAttributes', setAttributeOnDowncast: setViewAttributes }); documentListEditing.registerDowncastStrategy({ scope: 'list', attributeName: 'htmlUlAttributes', setAttributeOnDowncast: setViewAttributes }); documentListEditing.registerDowncastStrategy({ scope: 'list', attributeName: 'htmlOlAttributes', setAttributeOnDowncast: setViewAttributes }); dataFilter.on('register', (evt, definition) => { if (!viewElements.includes(definition.view)) { return; } evt.stop(); // Do not register same converters twice. if (schema.checkAttribute('$block', 'htmlLiAttributes')) { return; } const allowAttributes = viewElements.map(element => getHtmlAttributeName(element)); schema.extend('$block', { allowAttributes }); schema.extend('$blockObject', { allowAttributes }); schema.extend('$container', { allowAttributes }); conversion.for('upcast').add(dispatcher => { dispatcher.on('element:ul', viewToModelListAttributeConverter('htmlUlAttributes', dataFilter), { priority: 'low' }); dispatcher.on('element:ol', viewToModelListAttributeConverter('htmlOlAttributes', dataFilter), { priority: 'low' }); dispatcher.on('element:li', viewToModelListAttributeConverter('htmlLiAttributes', dataFilter), { priority: 'low' }); }); }); // Make sure that all items in a single list (items at the same level & listType) have the same properties. // Note: This is almost an exact copy from DocumentListPropertiesEditing. documentListEditing.on('postFixer', (evt, { listNodes, writer }) => { const previousNodesByIndent = []; // Last seen nodes of lower indented lists. for (const { node, previous } of listNodes) { // For the first list block there is nothing to compare with. if (!previous) { continue; } const nodeIndent = node.getAttribute('listIndent'); const previousNodeIndent = previous.getAttribute('listIndent'); let previousNodeInList = null; // It's like `previous` but has the same indent as current node. // Let's find previous node for the same indent. // We're going to need that when we get back to previous indent. if (nodeIndent > previousNodeIndent) { previousNodesByIndent[previousNodeIndent] = previous; } // Restore the one for given indent. else if (nodeIndent < previousNodeIndent) { previousNodeInList = previousNodesByIndent[nodeIndent]; previousNodesByIndent.length = nodeIndent; } // Same indent. else { previousNodeInList = previous; } // This is a first item of a nested list. if (!previousNodeInList) { continue; } if (previousNodeInList.getAttribute('listType') == node.getAttribute('listType')) { const attribute = getAttributeFromListType(previousNodeInList.getAttribute('listType')); const value = previousNodeInList.getAttribute(attribute); if (!isEqual(node.getAttribute(attribute), value) && writer.model.schema.checkAttribute(node, attribute)) { writer.setAttribute(attribute, value, node); evt.return = true; } } if (previousNodeInList.getAttribute('listItemId') == node.getAttribute('listItemId')) { const value = previousNodeInList.getAttribute('htmlLiAttributes'); if (!isEqual(node.getAttribute('htmlLiAttributes'), value) && writer.model.schema.checkAttribute(node, 'htmlLiAttributes')) { writer.setAttribute('htmlLiAttributes', value, node); evt.return = true; } } } }); // Remove `ol` attributes from `ul` elements and vice versa. documentListEditing.on('postFixer', (evt, { listNodes, writer }) => { for (const { node } of listNodes) { const listType = node.getAttribute('listType'); if (listType === 'bulleted' && node.getAttribute('htmlOlAttributes')) { writer.removeAttribute('htmlOlAttributes', node); evt.return = true; } if (listType === 'numbered' && node.getAttribute('htmlUlAttributes')) { writer.removeAttribute('htmlUlAttributes', node); evt.return = true; } } }); } /** * @inheritDoc */ afterInit() { const editor = this.editor; if (!editor.commands.get('indentList')) { return; } // Reset list attributes after indenting list items. const indentList = editor.commands.get('indentList'); this.listenTo(indentList, 'afterExecute', (evt, changedBlocks) => { editor.model.change(writer => { for (const node of changedBlocks) { const attribute = getAttributeFromListType(node.getAttribute('listType')); if (!editor.model.schema.checkAttribute(node, attribute)) { continue; } // Just reset the attribute. // If there is a previous indented list that this node should be merged into, // the postfixer will unify all the attributes of both sub-lists. writer.setAttribute(attribute, {}, node); } }); }); } } /** * View-to-model conversion helper preserving allowed attributes on {@link TODO} * feature model element. * * @returns Returns a conversion callback. */ function viewToModelListAttributeConverter(attributeName, dataFilter) { return (evt, data, conversionApi) => { const viewElement = data.viewItem; if (!data.modelRange) { Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor)); } const viewAttributes = dataFilter.processViewAttributes(viewElement, conversionApi); for (const item of data.modelRange.getItems({ shallow: true })) { // Apply only to list item blocks. if (!item.hasAttribute('listItemId')) { continue; } // Set list attributes only on same level items, those nested deeper are already handled // by the recursive conversion. if (item.hasAttribute(attributeName)) { continue; } if (conversionApi.writer.model.schema.checkAttribute(item, attributeName)) { conversionApi.writer.setAttribute(attributeName, viewAttributes || {}, item); } } }; } /** * Returns HTML attribute name based on provided list type. */ function getAttributeFromListType(listType) { return listType === 'bulleted' ? 'htmlUlAttributes' : 'htmlOlAttributes'; }