/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import ViewContainerElement from '../view/containerelement'; import ViewAttributeElement from '../view/attributeelement'; import ViewUIElement from '../view/uielement'; import { insertElement, wrap, changeAttribute, insertUIElement, removeUIElement, highlightText, highlightElement, removeHighlight } from './downcast-converters'; import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep'; /** * @module engine/conversion/downcast-helpers */ /** * Model element to view element conversion helper. * * This conversion results in creating a view element. For example, model `Foo` becomes `

Foo

` in the view. * * downcastElementToElement( { model: 'paragraph', view: 'p' } ); * * downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' ); * * downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } ); * * downcastElementToElement( { * model: 'fancyParagraph', * view: { * name: 'p', * class: 'fancy' * } * } ); * * downcastElementToElement( { * model: 'heading', * view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {String} config.model Name of the model element to convert. * @param {String|module:engine/view/elementdefinition~elementDefinition|Function| * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition, * or a function that takes model element as a parameter and returns a view container element, * or a view container element instance. The view element will be used then in conversion. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function downcastElementToElement( config, priority = 'normal' ) { config = cloneDeep( config ); _normalizeToElementConfig( config, ViewContainerElement ); return dispatcher => { dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } ); }; } /** * Model attribute to view element conversion helper. * * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data * `"Foo"` and `bold` attribute becomes `Foo` in the view. * * downcastAttributeToElement( 'bold', { view: 'strong' } ); * * downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' ); * * downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } ); * * downcastAttributeToElement( 'bold', { * view: { * name: 'span', * class: 'bold' * } * } ); * * downcastAttributeToElement( 'styled', { * model: 'dark', * view: { * name: 'span', * class: [ 'styled', 'styled-dark' ] * } * } ); * * downcastAttributeToElement( 'fontSize', [ * { * model: 'big', * view: { * name: 'span', * style: { * 'font-size': '1.2em' * } * } * }, * { * model: 'small', * view: { * name: 'span', * style: { * 'font-size': '0.8em' * } * } * } * ] ); * * downcastAttributeToElement( 'bold', { * view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ) * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {String} modelAttributeKey The key of the attribute to convert. * @param {Object|Array.} config Conversion configuration. It is possible to provide multiple configurations in an array. * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined. * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value. * @param {String|module:engine/view/elementdefinition~elementDefinition|Function| * module:engine/view/attributeelement~AttributeElement} config.view View element name, or a view element definition, * or a function that takes model element as a parameter and returns a view attribute element, or a view attribute element instance. * The view element will be used then in conversion. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) { config = cloneDeep( config ); _normalizeToElementConfig( config, ViewAttributeElement ); const elementCreator = _getCreatorForArrayConfig( config ); return dispatcher => { dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } ); }; } /** * Model attribute to view attribute conversion helper. * * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example, * `` is converted to ``. * * downcastAttributeToAttribute( 'src' ); * * downcastAttributeToAttribute( 'source', { view: 'src' } ); * * downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' ); * * downcastAttributeToAttribute( 'stylish', { * view: { * key: 'class', * value: 'styled' * } * } ); * * downcastAttributeToAttribute( 'styled', { * model: 'dark', * view: { * key: 'class', * value: 'styled styled-dark' * } * } ); * * downcastAttributeToAttribute( 'style', [ * { * model: 'dark', * view: { * key: 'class', * value: 'styled-dark' * } * }, * { * model: 'light', * view: { * key: 'class', * value: 'styled-light' * } * } * ] ); * * downcastAttributeToAttribute( 'style', { * view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } ) * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {String} modelAttributeKey The key of the attribute to convert. * @param {Object|Array.} [config] Conversion configuration. It is possible to provide multiple configurations in an array. * If not set, the conversion helper will assume 1-to-1 conversion, that is the view attribute key and view attribute value * will be same as model attribute key and model attribute value. * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined. * If `true` is provided, the configuration item will be used as a "default" configuration when no other item matches * the attribute value. * @param {String|Object|Function} [config.view] View attribute key, or an object with `key` and `value` properties (both `String`), * or a function that takes model attribute value and returns an object with `key` and `value` properties (both `String`). * If nothing is passed, the view attribute key and value will be equal to the model attribute key and value. * If a `String` is passed, it will be used as view attribute key and view attribute value will be equal to model attribute value. * If an object or a function returning an object is passed, its properties will be used to set view attribute key and value. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) { config = cloneDeep( config ); _normalizeToAttributeConfig( modelAttributeKey, config ); const elementCreator = _getCreatorForArrayConfig( config ); return dispatcher => { dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } ); }; } /** * Model marker to view element conversion helper. * * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker * is collapsed, only one element is created. For example, model marker set like this `F[oo b]ar` * becomes `

Foo bar

` in the view. * * downcastMarkerToElement( { model: 'search', view: 'marker-search' } ); * * downcastMarkerToElement( { model: 'search', view: 'marker-search' }, 'high' ); * * downcastMarkerToElement( { model: 'search', view: new ViewUIElement( 'span', { data-marker: 'search' } ) } ); * * downcastMarkerToElement( { * model: 'search', * view: { * name: 'span', * attribute: { * 'data-marker': 'search' * } * } * } ); * * downcastMarkerToElement( { * model: 'search', * view: data => { * return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } ); * } * } ); * * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}. * The `data` object properties are passed from * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to * marker end boundary element. * * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline. * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {String} config.model Name of the model marker (or model marker group) to convert. * @param {module:engine/view/elementdefinition~elementDefinition|Function} config.view View element definition * which will be used to build a view element for conversion or a function that takes model marker data as a parameter and * returns view element to use in conversion. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function downcastMarkerToElement( config, priority = 'normal' ) { config = cloneDeep( config ); _normalizeToElementConfig( config, ViewUIElement ); return dispatcher => { dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } ); dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } ); }; } /** * Model marker to highlight conversion helper. * * This conversion results in creating a highlight on view nodes. For this kind of conversion, * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided. * * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes * in the converted marker range. For example, model marker set like this `F[oo b]ar` becomes * `

Foo bar

` in the view. * * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often, * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`). * For example, model marker set like this `[]` becomes `` * in the view. * * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it * to a container element, it is the container element instance itself which applies values from highlight descriptor. * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that. * * downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } ); * * downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' ); * * downcastMarkerToHighlight( { * model: 'comment', * view: data => { * // Assuming that marker name is in a form of comment:commentType. * const commentType = data.markerName.split( ':' )[ 1 ]; * * return { * class: [ 'comment', 'comment-' + commentType ] * }; * } * } ); * * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}. * The `data` object properties are passed from * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {String} config.model Name of the model marker (or model marker group) to convert. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function downcastMarkerToHighlight( config, priority = 'normal' ) { return dispatcher => { dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority } ); dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority } ); dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority } ); }; } // Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters // for generating a view element. // // @param {Object} config Object with conversion helper configuration. // @param {Function} ViewElementClass View element class to use when generating view element from config. function _normalizeToElementConfig( config, ViewElementClass ) { // If config is given as an array, normalize each entry separately. if ( Array.isArray( config ) ) { for ( const configEntry of config ) { _normalizeToElementConfig( configEntry, ViewElementClass ); } return; } // Build `.view` property. // It is expected to be either creator function or view element instance. if ( typeof config.view == 'string' ) { // If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`. config.view = new ViewElementClass( config.view ); } else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) { // If `.view` is an object, use it to build view element instance. config.view = _createViewElementFromDefinition( config.view, ViewElementClass ); } // `.view` can be also a function or already a view element instance. // These are already valid types which don't have to be normalized. } // Creates view element instance from provided viewElementDefinition and class. // // @param {module:engine/view/elementdefinition~elementDefinition} viewElementDefinition // @param {Function} ViewElementClass // @returns {module:engine/view/element~Element} function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) { const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) ); if ( viewElementDefinition.style ) { element.setStyle( viewElementDefinition.style ); } if ( viewElementDefinition.class ) { const classes = viewElementDefinition.class; if ( typeof classes == 'string' ) { element.addClass( classes ); } else { element.addClass( ...classes ); } } return element; } // Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters // for generating view attribute. // // @param {String} modelAttributeKey Model attribute key for which config is defined. // @param {Object} [config] Config with conversion helper configuration. function _normalizeToAttributeConfig( modelAttributeKey, config ) { // If config is given as an array, normalize each entry separately. if ( Array.isArray( config ) ) { for ( const configEntry of config ) { _normalizeToAttributeConfig( modelAttributeKey, configEntry ); } return; } // Build `.view` property. // It is expected to be a creator function, that takes attribute value and model item and returns an object // with `key` property and `value` property which are view attribute key and view attribute value. if ( !config.view ) { // If `.view` is not set, take both attribute name and attribute value from model. const viewAttributeKey = modelAttributeKey; config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } ); } else if ( typeof config.view == 'string' ) { // If `.view` is set as a string, use it as a view attribute name. Value will be taken from model attribute value. const viewAttributeKey = config.view; config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } ); } else if ( typeof config.view == 'object' ) { // If `.view` is set as an object, use set key and value. const viewAttributeKey = config.view.key; const viewAttributeValue = config.view.value; config.view = () => ( { key: viewAttributeKey, value: viewAttributeValue } ); } // `.view` can be also already a function. } // Takes config and creates a view element creator function that chooses an appropriate entry from the config depending on // the value of model attribute. // // Supports specifying config as a single object or an array of objects. // Supports `.view` defined as an object and as a function. // // @param {Object|Array.} config Config with conversion helper configuration. function _getCreatorForArrayConfig( config ) { if ( !Array.isArray( config ) ) { config = [ config ]; } // Get "default config" entry. It is the entry with `.model` property set to `true`. // "Default" entry should be used if no other entry matched model attribute value. const defaultConfig = config.find( configEntry => configEntry.model === undefined ); // Return a creator function. return modelAttributeValue => { // Set default config at first. It will be used if no other entry matches model attribute value. let matchedConfigEntry = defaultConfig; // Creator should check all entries from the config... for ( const configEntry of config ) { if ( configEntry.model === modelAttributeValue ) { // If `.model` specified in entry matches converted attribute's value, choose it. matchedConfigEntry = configEntry; break; } } // If there was default config or matched config... if ( matchedConfigEntry ) { // If the entry `.view` is a function, execute it and return the value... if ( typeof matchedConfigEntry.view == 'function' ) { return matchedConfigEntry.view( modelAttributeValue ); } // Else, just return `.view`, it should be a view element instance after it got normalized earlier. return matchedConfigEntry.view; } return null; }; }