Browse Source

An initial implementation for the list styles feature.

Kamil Piechaczek 5 years ago
parent
commit
b6fa8b94a2

+ 36 - 0
packages/ckeditor5-list/src/liststyles.js

@@ -0,0 +1,36 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/liststyles
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ListStylesEditing from './liststylesediting';
+import ListStylesUI from './liststylesui';
+
+/**
+ * The to-do list feature.
+ *
+ * This is a "glue" plugin that loads the {@link module:list/liststylesediting~ListStylesEditing list styles editing feature}
+ * and the {@link module:list/liststylesolistui~ListStylesUI list styles UI feature}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ListStyles extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ListStylesEditing, ListStylesUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ListStyles';
+	}
+}

+ 163 - 0
packages/ckeditor5-list/src/liststylesediting.js

@@ -0,0 +1,163 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/liststylesediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ListEditing from './listediting';
+
+const DEFAULT_TYPE = 'default';
+
+/**
+ * The list styles engine feature.
+ *
+ * It sets value for the `listItem` attribute for the {@link module:list/list~List `<listItem>`} element that
+ * allows modifying list style type.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ListStylesEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ListEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ListStylesEditing';
+	}
+
+	init() {
+		const editor = this.editor;
+		const model = editor.model;
+
+		// Extend schema.
+		model.schema.extend( 'listItem', {
+			allowAttributes: [ 'listStyle' ]
+		} );
+
+		// Disallow the `listStyle` attribute on to-do lists.
+		model.schema.addAttributeCheck( ( context, attributeName ) => {
+			const item = context.last;
+
+			if ( attributeName == 'listStyle' && item.name == 'listItem' && item.getAttribute( 'listType' ) == 'todo' ) {
+				return false;
+			}
+		} );
+
+		// Set up conversion.
+		editor.conversion.for( 'upcast' ).add( upcastListItem() );
+		editor.conversion.for( 'downcast' ).add( downcastListStyleAttribute() );
+	}
+}
+
+// Returns a converter that consumes the `style` attribute and search for `list-style-type` definition.
+// If not found, the `"default"` value will be used.
+//
+// @private
+// @returns {Function}
+function upcastListItem() {
+	return dispatcher => {
+		dispatcher.on( 'element:li', ( evt, data, conversionApi ) => {
+			if ( !conversionApi.consumable.consume( data.viewItem, { attributes: [ 'style' ] } ) ) {
+				return;
+			}
+
+			const listParent = data.viewItem.parent;
+			const listStyle = listParent.getStyle( 'list-style-type' ) || DEFAULT_TYPE;
+			const listItem = data.modelRange.start.nodeAfter;
+
+			conversionApi.writer.setAttribute( 'listStyle', listStyle, listItem );
+		}, { priority: 'low' } );
+	};
+}
+
+// Returns a converter that adds the `list-style-type` definition as a value for the `style` attribute.
+// The `"default"` value is removed and not present in the view/data.
+//
+// @private
+// @returns {Function}
+function downcastListStyleAttribute() {
+	return dispatcher => {
+		dispatcher.on( 'attribute:listStyle:listItem', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const currentItem = data.item;
+			const previousItem = currentItem.previousSibling;
+			const viewItem = conversionApi.mapper.toViewElement( currentItem );
+			const listStyle = data.attributeNewValue;
+
+			// Parsing the first element in a list. Just set the attribute.
+			if ( !previousItem || !previousItem.is( 'element', 'listItem' ) ) {
+				return setListStyle( viewWriter, listStyle, viewItem.parent );
+			}
+
+			// But if previous element is the list item, we must be sure that those two items belong to the same list.
+			// So, we should check whether the values of the `listType`, `listIndent` and `listStyle` attributes are equal.
+			//
+			// If the current parsed list item does not belong to the same list that the previous element,
+			// the `listStyle` attribute must be set once again since another list is being processed.
+			//
+			// Note: We ignore the check of the `listStyle` attribute since that case must be handled another way.
+			// If two items have the same values for `listType` and `listIndent` but not for `listStyle`,
+			// we must split the list container (`<ol>` or `<ul>`) since we're processing two different lists.
+			if ( !areRepresentingSameList( previousItem, currentItem ) ) {
+				return setListStyle( viewWriter, listStyle, viewItem.parent );
+			}
+
+			const previousListStyle = previousItem.getAttribute( 'listStyle' );
+
+			// Since we were ignoring the `listStyle` check, it must be checked before splitting the list container.
+			// No change is needed if previous element has the same value of the `listStyle` attribute.
+			if ( previousListStyle === listStyle ) {
+				return;
+			}
+
+			// But if those attributes are different, we must split the parent element
+			// and set the attribute for the new created container.
+			viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
+			viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
+
+			setListStyle( viewWriter, listStyle, viewItem.parent );
+		}, { priority: 'low' } );
+	};
+
+	// Checks whether specified list items belong to the same list.
+	//
+	// Comparing the `listStyle` attribute is by design since it requires additional actions.
+	//
+	// @param {module:engine/model/element~Element} listItem1 The first list item to check.
+	// @param {module:engine/model/element~Element} listItem2 The second list item to check.
+	// @returns {Boolean}
+	function areRepresentingSameList( listItem1, listItem2 ) {
+		if ( listItem1.getAttribute( 'listType' ) !== listItem2.getAttribute( 'listType' ) ) {
+			return false;
+		}
+
+		if ( listItem1.getAttribute( 'listIndent' ) !== listItem2.getAttribute( 'listIndent' ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	// Updates or removes the `list-style-type` from the `element`.
+	//
+	// @param {module:engine/view/downcastwriter~DowncastWriter} writer
+	// @param {String} listStyle
+	// @param {module:engine/view/element~Element} element
+	function setListStyle( writer, listStyle, element ) {
+		if ( listStyle && listStyle !== DEFAULT_TYPE ) {
+			writer.setStyle( 'list-style-type', listStyle, element );
+		} else {
+			writer.removeStyle( 'list-style-type', element );
+		}
+	}
+}

+ 26 - 0
packages/ckeditor5-list/src/liststylesui.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/liststylesui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class ListStylesUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ListStylesUI';
+	}
+
+	init() {
+		return null;
+	}
+}