| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
- *
- * @module engine/conversion/downcasthelpers
- */
- import ModelRange from '../model/range';
- import ModelSelection from '../model/selection';
- import ModelElement from '../model/element';
- import ViewAttributeElement from '../view/attributeelement';
- import DocumentSelection from '../model/documentselection';
- import ConversionHelpers from './conversionhelpers';
- import { cloneDeep } from 'lodash-es';
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- /**
- * Downcast conversion helper functions.
- *
- * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
- */
- export default class DowncastHelpers extends ConversionHelpers {
- /**
- * Model element to view element conversion helper.
- *
- * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
- *
- * editor.conversion.for( 'downcast' ).elementToElement( {
- * model: 'paragraph',
- * view: 'p'
- * } );
- *
- * editor.conversion.for( 'downcast' ).elementToElement( {
- * model: 'paragraph',
- * view: 'div',
- * converterPriority: 'high'
- * } );
- *
- * editor.conversion.for( 'downcast' ).elementToElement( {
- * model: 'fancyParagraph',
- * view: {
- * name: 'p',
- * classes: 'fancy'
- * }
- * } );
- *
- * editor.conversion.for( 'downcast' ).elementToElement( {
- * model: 'heading',
- * view: ( modelElement, conversionApi ) => {
- * const { writer } = conversionApi;
- *
- * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
- * }
- * } );
- *
- * The element-to-element conversion supports a reconversion mechanism. This is helpful in conversion to complex view structures where
- * multiple atomic element-to-element and attribute-to-attribute or attribute-to-element could be used. By specifying `triggerBy`
- * events you can trigger reconverting model to a full view tree structures at once.
- *
- * editor.conversion.for( 'downcast' ).elementToElement( {
- * model: 'complex',
- * view: ( modelElement, conversionApi ) => createComplexViewFromModel( modelElement, conversionApi ),
- * triggerBy: {
- * attributes: [ 'foo', 'bar' ],
- * children: [ 'slot' ]
- * }
- * } );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * You can read more about element-to-element conversion in the
- * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion} guide.
- *
- * @method #elementToElement
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model element to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
- * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
- * as parameters and returns a view container element.
- * @param {Object} [config.triggerBy] Re-conversion triggers. At least one trigger must be defined.
- * @param {Array.<String>} config.triggerBy.attributes Name of element's attributes which change will trigger element reconversion.
- * @param {Array.<String>} config.triggerBy.children Name of direct children that adding or removing will trigger element reconversion.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- elementToElement( config ) {
- return this.add( downcastElementToElement( config ) );
- }
- /**
- * Model attribute to view element conversion helper.
- *
- * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
- * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: 'bold',
- * view: 'strong'
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: 'bold',
- * view: 'b',
- * converterPriority: 'high'
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: 'invert',
- * view: {
- * name: 'span',
- * classes: [ 'font-light', 'bg-dark' ]
- * }
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: {
- * key: 'fontSize',
- * values: [ 'big', 'small' ]
- * },
- * view: {
- * big: {
- * name: 'span',
- * styles: {
- * 'font-size': '1.2em'
- * }
- * },
- * small: {
- * name: 'span',
- * styles: {
- * 'font-size': '0.8em'
- * }
- * }
- * }
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: 'bold',
- * view: ( modelAttributeValue, conversionApi ) => {
- * const { writer } = conversionApi;
- *
- * return writer.createAttributeElement( 'span', {
- * style: 'font-weight:' + modelAttributeValue
- * } );
- * }
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: {
- * key: 'color',
- * name: '$text'
- * },
- * view: ( modelAttributeValue, conversionApi ) => {
- * const { writer } = conversionApi;
- *
- * return writer.createAttributeElement( 'span', {
- * style: 'color:' + modelAttributeValue
- * } );
- * }
- * } );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @method #attributeToElement
- * @param {Object} config Conversion configuration.
- * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
- * of `String`s with possible values if the model attribute is an enumerable.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
- * that takes the model attribute value and
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
- * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
- * to view element definitions or functions.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- attributeToElement( config ) {
- return this.add( downcastAttributeToElement( config ) );
- }
- /**
- * Model attribute to view attribute conversion helper.
- *
- * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
- * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: 'source',
- * view: 'src'
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: 'source',
- * view: 'href',
- * converterPriority: 'high'
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: {
- * name: 'image',
- * key: 'source'
- * },
- * view: 'src'
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: {
- * name: 'styled',
- * values: [ 'dark', 'light' ]
- * },
- * view: {
- * dark: {
- * key: 'class',
- * value: [ 'styled', 'styled-dark' ]
- * },
- * light: {
- * key: 'class',
- * value: [ 'styled', 'styled-light' ]
- * }
- * }
- * } );
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: 'styled',
- * view: modelAttributeValue => ( {
- * key: 'class',
- * value: 'styled-' + modelAttributeValue
- * } )
- * } );
- *
- * **Note**: Downcasting to a style property requires providing `value` as an object:
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: 'lineHeight',
- * view: modelAttributeValue => ( {
- * key: 'style',
- * value: {
- * 'line-height': modelAttributeValue,
- * 'border-bottom': '1px dotted #ba2'
- * }
- * } )
- * } );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @method #attributeToAttribute
- * @param {Object} config Conversion configuration.
- * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
- * the attribute key, possible values and, optionally, an element name to convert from.
- * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
- * the model attribute value and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
- * as parameters and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
- * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
- * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
- * `{ key, value }` objects or a functions.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- attributeToAttribute( config ) {
- return this.add( downcastAttributeToAttribute( config ) );
- }
- /**
- * Model marker to view element conversion helper.
- *
- * **Note**: This method should be used only for editing downcast. For data downcast, use
- * {@link #markerToData `#markerToData()`} that produces valid HTML data.
- *
- * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
- * is collapsed, only one element is created. For example, model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
- * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
- *
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
- * model: 'search',
- * view: 'marker-search'
- * } );
- *
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
- * model: 'search',
- * view: 'search-result',
- * converterPriority: 'high'
- * } );
- *
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
- * model: 'search',
- * view: {
- * name: 'span',
- * attributes: {
- * 'data-marker': 'search'
- * }
- * }
- * } );
- *
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
- * model: 'search',
- * view: ( markerData, conversionApi ) => {
- * const { writer } = conversionApi;
- *
- * return writer.createUIElement( 'span', {
- * 'data-marker': 'search',
- * 'data-start': markerData.isOpening
- * } );
- * }
- * } );
- *
- * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
- * as a parameters and should return an instance of the
- * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
- * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
- * the marker end boundary element.
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @method #markerToElement
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model marker (or model marker group) to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function that
- * takes the model marker data and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
- * as a parameters and returns a view UI element.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- markerToElement( config ) {
- return this.add( downcastMarkerToElement( config ) );
- }
- /**
- * 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/downcasthelpers~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, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
- * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
- *
- * {@link module:engine/view/containerelement~ContainerElement} may provide a 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, a model marker set like this: `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
- * in the view.
- *
- * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
- * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
- * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
- *
- * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
- *
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
- * model: 'comment',
- * view: { classes: 'new-comment' },
- * converterPriority: 'high'
- * } );
- *
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
- * model: 'comment',
- * view: ( data, converstionApi ) => {
- * // Assuming that the marker name is in a form of comment:commentType.
- * const commentType = data.markerName.split( ':' )[ 1 ];
- *
- * return {
- * classes: [ 'comment', 'comment-' + commentType ]
- * };
- * }
- * } );
- *
- * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
- * as a parameters and should return a
- * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
- * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @method #markerToHighlight
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model marker (or model marker group) to convert.
- * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
- * that will be used for highlighting or a function that takes the model marker data and
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
- * and returns a highlight descriptor.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- markerToHighlight( config ) {
- return this.add( downcastMarkerToHighlight( config ) );
- }
- /**
- * Model marker converter for data downcast.
- *
- * This conversion creates a representation for model marker boundaries in the view:
- *
- * * If the marker boundary is at a position where text nodes are allowed, then a view element with the specified tag name
- * and `name` attribute is added at this position.
- * * In other cases, a specified attribute is set on a view element that is before or after the marker boundary.
- *
- * Typically, marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
- * The default configuration for this conversion is that the first part is the `group` part and the rest of
- * the marker name becomes the `name` part.
- *
- * Tag and attribute names and values are generated from the marker name:
- *
- * * Templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
- * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
- * * Templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
- *
- * Attributes mark whether the given marker's start or end boundary is before or after the given element.
- * Attributes `data-[group]-start-before` and `data-[group]-end-after` are favored.
- * The other two are used when the former two cannot be used.
- *
- * The conversion configuration can take a function that will generate different group and name parts.
- * If such function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
- * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
- *
- * Basic usage:
- *
- * // Using the default conversion.
- * // In this case, all markers whose name starts with 'comment:' will be converted.
- * // The `group` parameter will be set to `comment`.
- * // The `name` parameter will be the rest of the marker name (without `:`).
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
- * model: 'comment'
- * } );
- *
- * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
- * by `[]`):
- *
- * // Model:
- * <paragraph>Foo[bar</paragraph>
- * <image src="abc.jpg"></image>]
- *
- * // View:
- * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
- * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
- *
- * In the example above, the comment starts before "bar" and ends after the image.
- *
- * If the `name` part is empty, the following view may be generated:
- *
- * <p>Foo <myMarker-start></myMarker-start>bar</p>
- * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
- *
- * **Note:** A situation where some markers have the `name` part and some do not have it is incorrect and should be avoided.
- *
- * Examples where `data-group-start-after` and `data-group-end-before` are used:
- *
- * // Model:
- * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
- *
- * // View:
- * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
- *
- * Similarly, when a marker is collapsed after the last element:
- *
- * // Model:
- * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
- *
- * // View:
- * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
- *
- * When there are multiple markers from the same group stored in the same attribute of the same element, their
- * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
- *
- * Other examples of usage:
- *
- * // Using a custom function which is the same as the default conversion:
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
- * model: 'comment'
- * view: markerName => ( {
- * group: 'comment',
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
- * } )
- * } );
- *
- * // Using the converter priority:
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
- * model: 'comment'
- * view: markerName => ( {
- * group: 'comment',
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
- * } ),
- * converterPriority: 'high'
- * } );
- *
- * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @method #markerToData
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model marker (or model marker group) to convert.
- * @param {Function} [config.view] A function that takes the model marker name and
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
- * and returns an object with the `group` and `name` properties.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
- */
- markerToData( config ) {
- return this.add( downcastMarkerToData( config ) );
- }
- }
- /**
- * Function factory that creates a default downcast converter for text insertion changes.
- *
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
- *
- * modelDispatcher.on( 'insert:$text', insertText() );
- *
- * @returns {Function} Insert text event converter.
- */
- export function insertText() {
- return ( evt, data, conversionApi ) => {
- if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
- return;
- }
- const viewWriter = conversionApi.writer;
- const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
- const viewText = viewWriter.createText( data.item.data );
- viewWriter.insert( viewPosition, viewText );
- };
- }
- /**
- * Function factory that creates a default downcast converter for node remove changes.
- *
- * modelDispatcher.on( 'remove', remove() );
- *
- * @returns {Function} Remove event converter.
- */
- export function remove() {
- return ( evt, data, conversionApi ) => {
- // Find view range start position by mapping model position at which the remove happened.
- const viewStart = conversionApi.mapper.toViewPosition( data.position );
- const modelEnd = data.position.getShiftedBy( data.length );
- const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
- const viewRange = conversionApi.writer.createRange( viewStart, viewEnd );
- // Trim the range to remove in case some UI elements are on the view range boundaries.
- const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
- // After the range is removed, unbind all view elements from the model.
- // Range inside view document fragment is used to unbind deeply.
- for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
- conversionApi.mapper.unbindViewElement( child );
- }
- };
- }
- /**
- * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
- * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority
- * is not provided in the descriptor, the default priority will be used.
- *
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
- * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
- * @returns {module:engine/view/attributeelement~AttributeElement}
- */
- export function createViewElementFromHighlightDescriptor( writer, descriptor ) {
- const viewElement = writer.createAttributeElement( 'span', descriptor.attributes );
- if ( descriptor.classes ) {
- viewElement._addClass( descriptor.classes );
- }
- if ( descriptor.priority ) {
- viewElement._priority = descriptor.priority;
- }
- viewElement._id = descriptor.id;
- return viewElement;
- }
- /**
- * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
- * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
- * value from the `consumable` object and maps model positions from the selection to view positions.
- *
- * modelDispatcher.on( 'selection', convertRangeSelection() );
- *
- * @returns {Function} Selection converter.
- */
- export function convertRangeSelection() {
- return ( evt, data, conversionApi ) => {
- const selection = data.selection;
- if ( selection.isCollapsed ) {
- return;
- }
- if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
- return;
- }
- const viewRanges = [];
- for ( const range of selection.getRanges() ) {
- const viewRange = conversionApi.mapper.toViewRange( range );
- viewRanges.push( viewRange );
- }
- conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } );
- };
- }
- /**
- * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
- * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
- * value from the `consumable` object, maps the model selection position to the view position and breaks
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
- *
- * modelDispatcher.on( 'selection', convertCollapsedSelection() );
- *
- * An example of the view state before and after converting the collapsed selection:
- *
- * <p><strong>f^oo<strong>bar</p>
- * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
- *
- * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
- * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
- * with different, appropriate attribute elements.
- *
- * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
- * by merging attributes.
- *
- * @returns {Function} Selection converter.
- */
- export function convertCollapsedSelection() {
- return ( evt, data, conversionApi ) => {
- const selection = data.selection;
- if ( !selection.isCollapsed ) {
- return;
- }
- if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
- return;
- }
- const viewWriter = conversionApi.writer;
- const modelPosition = selection.getFirstPosition();
- const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
- const brokenPosition = viewWriter.breakAttributes( viewPosition );
- viewWriter.setSelection( brokenPosition );
- };
- }
- /**
- * Function factory that creates a converter which clears artifacts after the previous
- * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
- * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
- * positions of all ranges.
- *
- * <p><strong>^</strong></p>
- * -> <p>^</p>
- *
- * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
- * -> <p><strong>foo^bar<strong>bar</p>
- *
- * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
- * -> <p><strong>foo^bar<strong>bar</p>
- *
- * This listener should be assigned before any converter for the new selection:
- *
- * modelDispatcher.on( 'selection', clearAttributes() );
- *
- * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
- * which does the opposite by breaking attributes in the selection position.
- *
- * @returns {Function} Selection converter.
- */
- export function clearAttributes() {
- return ( evt, data, conversionApi ) => {
- const viewWriter = conversionApi.writer;
- const viewSelection = viewWriter.document.selection;
- for ( const range of viewSelection.getRanges() ) {
- // Not collapsed selection should not have artifacts.
- if ( range.isCollapsed ) {
- // Position might be in the node removed by the view writer.
- if ( range.end.parent.isAttached() ) {
- conversionApi.writer.mergeAttributes( range.start );
- }
- }
- }
- viewWriter.setSelection( null );
- };
- }
- /**
- * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
- * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
- * selection will be put inside it.
- *
- * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
- * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
- * but are represented as an element in the view:
- *
- * [paragraph] MODEL ====> VIEW <p>
- * |- a {bold: true} |- <b>
- * |- b {bold: true} | |- ab
- * |- c |- c
- *
- * Passed `Function` will be provided with the attribute value and then all the parameters of the
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
- * The result of the function will be the wrapping element.
- * When the provided `Function` does not return any element, no conversion will take place.
- *
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
- *
- * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
- * return writer.createAttributeElement( 'strong' );
- * } );
- *
- * @protected
- * @param {Function} elementCreator Function returning a view element that will be used for wrapping.
- * @returns {Function} Set/change attribute converter.
- */
- export function wrap( elementCreator ) {
- return ( evt, data, conversionApi ) => {
- // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
- // or the attribute was removed.
- const oldViewElement = elementCreator( data.attributeOldValue, conversionApi );
- // Create node to wrap with.
- const newViewElement = elementCreator( data.attributeNewValue, conversionApi );
- if ( !oldViewElement && !newViewElement ) {
- return;
- }
- if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
- return;
- }
- const viewWriter = conversionApi.writer;
- const viewSelection = viewWriter.document.selection;
- if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
- // Selection attribute conversion.
- viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
- } else {
- // Node attribute conversion.
- let viewRange = conversionApi.mapper.toViewRange( data.range );
- // First, unwrap the range from current wrapper.
- if ( data.attributeOldValue !== null && oldViewElement ) {
- viewRange = viewWriter.unwrap( viewRange, oldViewElement );
- }
- if ( data.attributeNewValue !== null && newViewElement ) {
- viewWriter.wrap( viewRange, newViewElement );
- }
- }
- };
- }
- /**
- * Function factory that creates a converter which converts node insertion changes from the model to the view.
- * The function passed will be provided with all the parameters of the dispatcher's
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
- * The result of the function will be inserted into the view.
- *
- * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements.
- *
- * downcastDispatcher.on(
- * 'insert:myElem',
- * insertElement( ( modelItem, { writer } ) => {
- * const text = writer.createText( 'myText' );
- * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
- *
- * // Do something fancy with `myElem` using `modelItem` or other parameters.
- *
- * return myElem;
- * }
- * ) );
- *
- * @protected
- * @param {Function} elementCreator Function returning a view element, which will be inserted.
- * @returns {Function} Insert element event converter.
- */
- export function insertElement( elementCreator ) {
- return ( evt, data, conversionApi ) => {
- const viewElement = elementCreator( data.item, conversionApi );
- if ( !viewElement ) {
- return;
- }
- if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
- return;
- }
- const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
- conversionApi.mapper.bindElements( data.item, viewElement );
- conversionApi.writer.insert( viewPosition, viewElement );
- };
- }
- /**
- * Function factory that creates a converter which converts marker adding change to the
- * {@link module:engine/view/uielement~UIElement view UI element}.
- *
- * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
- * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
- * and at the end of the range.
- *
- * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
- *
- * @protected
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
- * that will be inserted.
- * @returns {Function} Insert element event converter.
- */
- export function insertUIElement( elementCreator ) {
- return ( evt, data, conversionApi ) => {
- // Create two view elements. One will be inserted at the beginning of marker, one at the end.
- // If marker is collapsed, only "opening" element will be inserted.
- data.isOpening = true;
- const viewStartElement = elementCreator( data, conversionApi );
- data.isOpening = false;
- const viewEndElement = elementCreator( data, conversionApi );
- if ( !viewStartElement || !viewEndElement ) {
- return;
- }
- const markerRange = data.markerRange;
- // Marker that is collapsed has consumable build differently that non-collapsed one.
- // For more information see `addMarker` event description.
- // If marker's range is collapsed - check if it can be consumed.
- if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
- return;
- }
- // If marker's range is not collapsed - consume all items inside.
- for ( const value of markerRange ) {
- if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
- return;
- }
- }
- const mapper = conversionApi.mapper;
- const viewWriter = conversionApi.writer;
- // Add "opening" element.
- viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
- conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
- // Add "closing" element only if range is not collapsed.
- if ( !markerRange.isCollapsed ) {
- viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
- conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
- }
- evt.stop();
- };
- }
- // Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
- // based on marker remove change.
- //
- // This converter unbinds elements from the marker name.
- //
- // @returns {Function} Removed UI element converter.
- function removeUIElement() {
- return ( evt, data, conversionApi ) => {
- const elements = conversionApi.mapper.markerNameToElements( data.markerName );
- if ( !elements ) {
- return;
- }
- for ( const element of elements ) {
- conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
- conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
- }
- conversionApi.writer.clearClonedElementsGroup( data.markerName );
- evt.stop();
- };
- }
- // Function factory that creates a default converter for model markers.
- //
- // See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
- //
- // This converter binds created UI elements and affected view elements with the marker name
- // using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
- //
- // @returns {Function} Add marker converter.
- function insertMarkerData( viewCreator ) {
- return ( evt, data, conversionApi ) => {
- const viewMarkerData = viewCreator( data.markerName, conversionApi );
- if ( !viewMarkerData ) {
- return;
- }
- const markerRange = data.markerRange;
- if ( !conversionApi.consumable.consume( markerRange, evt.name ) ) {
- return;
- }
- // Adding closing data first to keep the proper order in the view.
- handleMarkerBoundary( markerRange, false, conversionApi, data, viewMarkerData );
- handleMarkerBoundary( markerRange, true, conversionApi, data, viewMarkerData );
- evt.stop();
- };
- }
- // Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
- function handleMarkerBoundary( range, isStart, conversionApi, data, viewMarkerData ) {
- const modelPosition = isStart ? range.start : range.end;
- const canInsertElement = conversionApi.schema.checkChild( modelPosition, '$text' );
- if ( canInsertElement ) {
- const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
- insertMarkerAsElement( viewPosition, isStart, conversionApi, data, viewMarkerData );
- } else {
- let modelElement;
- let isBefore;
- // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
- // Below `if` is constructed in a way that will favor adding these attributes.
- //
- // Also, I assume that there will be always an element either after or before the position.
- // If not, then it is a case when we are not in a position where text is allowed and also there are no elements around...
- if ( isStart && modelPosition.nodeAfter || !isStart && !modelPosition.nodeBefore ) {
- modelElement = modelPosition.nodeAfter;
- isBefore = true;
- } else {
- modelElement = modelPosition.nodeBefore;
- isBefore = false;
- }
- const viewElement = conversionApi.mapper.toViewElement( modelElement );
- insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData );
- }
- }
- // Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
- function insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData ) {
- const attributeName = `data-${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }-${ isBefore ? 'before' : 'after' }`;
- const markerNames = viewElement.hasAttribute( attributeName ) ? viewElement.getAttribute( attributeName ).split( ',' ) : [];
- // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
- markerNames.unshift( viewMarkerData.name );
- conversionApi.writer.setAttribute( attributeName, markerNames.join( ',' ), viewElement );
- conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
- }
- // Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
- function insertMarkerAsElement( position, isStart, conversionApi, data, viewMarkerData ) {
- const viewElementName = `${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }`;
- const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
- const viewElement = conversionApi.writer.createUIElement( viewElementName, attrs );
- conversionApi.writer.insert( position, viewElement );
- conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
- }
- // Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
- //
- // @returns {Function} Remove marker converter.
- function removeMarkerData( viewCreator ) {
- return ( evt, data, conversionApi ) => {
- const viewData = viewCreator( data.markerName, conversionApi );
- if ( !viewData ) {
- return;
- }
- const elements = conversionApi.mapper.markerNameToElements( data.markerName );
- if ( !elements ) {
- return;
- }
- for ( const element of elements ) {
- conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
- if ( element.is( 'containerElement' ) ) {
- removeMarkerFromAttribute( `data-${ viewData.group }-start-before`, element );
- removeMarkerFromAttribute( `data-${ viewData.group }-start-after`, element );
- removeMarkerFromAttribute( `data-${ viewData.group }-end-before`, element );
- removeMarkerFromAttribute( `data-${ viewData.group }-end-after`, element );
- } else {
- conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
- }
- }
- conversionApi.writer.clearClonedElementsGroup( data.markerName );
- evt.stop();
- function removeMarkerFromAttribute( attributeName, element ) {
- if ( element.hasAttribute( attributeName ) ) {
- const markerNames = new Set( element.getAttribute( attributeName ).split( ',' ) );
- markerNames.delete( viewData.name );
- if ( markerNames.size == 0 ) {
- conversionApi.writer.removeAttribute( attributeName, element );
- } else {
- conversionApi.writer.setAttribute( attributeName, Array.from( markerNames ).join( ',' ), element );
- }
- }
- }
- };
- }
- // Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
- //
- // Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
- // a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
- // attributes on a one-to-one basis.
- //
- // *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
- //
- // The converter automatically consumes the corresponding value from the consumables list and stops the event (see
- // {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
- //
- // modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
- // // Change attribute key from `customAttr` to `class` in the view.
- // const key = 'class';
- // let value = data.attributeNewValue;
- //
- // // Force attribute value to 'empty' if the model element is empty.
- // if ( data.item.childCount === 0 ) {
- // value = 'empty';
- // }
- //
- // // Return the key-value pair.
- // return { key, value };
- // } ) );
- //
- // @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
- // represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- // The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
- // @returns {Function} Set/change attribute converter.
- function changeAttribute( attributeCreator ) {
- return ( evt, data, conversionApi ) => {
- const oldAttribute = attributeCreator( data.attributeOldValue, conversionApi );
- const newAttribute = attributeCreator( data.attributeNewValue, conversionApi );
- if ( !oldAttribute && !newAttribute ) {
- return;
- }
- if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
- return;
- }
- const viewElement = conversionApi.mapper.toViewElement( data.item );
- const viewWriter = conversionApi.writer;
- // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
- // Only elements can have attributes in a view so do not proceed for anything else (#1587).
- if ( !viewElement ) {
- /**
- * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
- * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
- * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
- *
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
- * model: 'attribute-name',
- * view: 'attribute-name'
- * } ) );
- *
- * and given attribute is used on text node, for example:
- *
- * model.change( writer => {
- * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
- * } );
- *
- * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
- * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
- * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
- * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
- *
- * editor.conversion.for( 'downcast' ).attributeToElement( {
- * model: {
- * key: 'attribute-name',
- * name: '$text'
- * },
- * view: ( value, { writer } ) => {
- * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
- * },
- * converterPriority: 'high'
- * } ) );
- *
- * @error conversion-attribute-to-attribute-on-text
- */
- throw new CKEditorError(
- 'conversion-attribute-to-attribute-on-text',
- [ data, conversionApi ]
- );
- }
- // First remove the old attribute if there was one.
- if ( data.attributeOldValue !== null && oldAttribute ) {
- if ( oldAttribute.key == 'class' ) {
- const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
- for ( const className of classes ) {
- viewWriter.removeClass( className, viewElement );
- }
- } else if ( oldAttribute.key == 'style' ) {
- const keys = Object.keys( oldAttribute.value );
- for ( const key of keys ) {
- viewWriter.removeStyle( key, viewElement );
- }
- } else {
- viewWriter.removeAttribute( oldAttribute.key, viewElement );
- }
- }
- // Then set the new attribute.
- if ( data.attributeNewValue !== null && newAttribute ) {
- if ( newAttribute.key == 'class' ) {
- const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
- for ( const className of classes ) {
- viewWriter.addClass( className, viewElement );
- }
- } else if ( newAttribute.key == 'style' ) {
- const keys = Object.keys( newAttribute.value );
- for ( const key of keys ) {
- viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
- }
- } else {
- viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
- }
- }
- };
- }
- // Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
- // {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
- // See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
- //
- // It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
- // created and the selection will be put inside it.
- //
- // If the highlight descriptor does not provide the `priority` property, `10` will be used.
- //
- // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- //
- // This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
- // using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
- //
- // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
- // @returns {Function}
- function highlightText( highlightDescriptor ) {
- return ( evt, data, conversionApi ) => {
- if ( !data.item ) {
- return;
- }
- if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( '$textProxy' ) ) {
- return;
- }
- const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
- if ( !descriptor ) {
- return;
- }
- if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
- return;
- }
- const viewWriter = conversionApi.writer;
- const viewElement = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
- const viewSelection = viewWriter.document.selection;
- if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
- viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
- } else {
- const viewRange = conversionApi.mapper.toViewRange( data.range );
- const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
- for ( const element of rangeAfterWrap.getItems() ) {
- if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
- conversionApi.mapper.bindElementToMarker( element, data.markerName );
- // One attribute element is enough, because all of them are bound together by the view writer.
- // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
- break;
- }
- }
- }
- };
- }
- // Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
- //
- // The converter checks if an element has the `addHighlight` function stored as a
- // {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
- // In such case the converter will consume all element's children, assuming that they were handled by the element itself.
- //
- // When the `addHighlight` custom property is not present, the element is not converted in any special way.
- // This means that converters will proceed to convert the element's child nodes.
- //
- // If the highlight descriptor does not provide the `priority` property, `10` will be used.
- //
- // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- //
- // This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
- // the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
- //
- // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
- // @returns {Function}
- function highlightElement( highlightDescriptor ) {
- return ( evt, data, conversionApi ) => {
- if ( !data.item ) {
- return;
- }
- if ( !( data.item instanceof ModelElement ) ) {
- return;
- }
- const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
- if ( !descriptor ) {
- return;
- }
- if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
- return;
- }
- const viewElement = conversionApi.mapper.toViewElement( data.item );
- if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
- // Consume element itself.
- conversionApi.consumable.consume( data.item, evt.name );
- // Consume all children nodes.
- for ( const value of ModelRange._createIn( data.item ) ) {
- conversionApi.consumable.consume( value.item, evt.name );
- }
- viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
- conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
- }
- };
- }
- // Function factory that creates a converter which converts the removing model marker to the view.
- //
- // Both text nodes and elements are handled by this converter but they are handled a bit differently.
- //
- // Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
- // provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
- //
- // For elements, the converter checks if an element has the `removeHighlight` function stored as a
- // {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
- // In such case, the children of that element will not be converted.
- //
- // When `removeHighlight` is not present, the element is not converted in any special way.
- // The converter will proceed to convert the element's child nodes instead.
- //
- // If the highlight descriptor does not provide the `priority` property, `10` will be used.
- //
- // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- //
- // This converter unbinds elements from the marker name.
- //
- // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
- // @returns {Function}
- function removeHighlight( highlightDescriptor ) {
- return ( evt, data, conversionApi ) => {
- // This conversion makes sense only for non-collapsed range.
- if ( data.markerRange.isCollapsed ) {
- return;
- }
- const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
- if ( !descriptor ) {
- return;
- }
- // View element that will be used to unwrap `AttributeElement`s.
- const viewHighlightElement = createViewElementFromHighlightDescriptor( conversionApi.writer, descriptor );
- // Get all elements bound with given marker name.
- const elements = conversionApi.mapper.markerNameToElements( data.markerName );
- if ( !elements ) {
- return;
- }
- for ( const element of elements ) {
- conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
- if ( element.is( 'attributeElement' ) ) {
- conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
- } else {
- // if element.is( 'containerElement' ).
- element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
- }
- }
- conversionApi.writer.clearClonedElementsGroup( data.markerName );
- evt.stop();
- };
- }
- // Model element to view element conversion helper.
- //
- // See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
- //
- // @param {Object} config Conversion configuration.
- // @param {String} config.model
- // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view
- // @param {Object} [config.triggerBy]
- // @param {Array.<String>} [config.triggerBy.attributes]
- // @param {Array.<String>} [config.triggerBy.children]
- // @returns {Function} Conversion helper.
- function downcastElementToElement( config ) {
- config = cloneDeep( config );
- config.view = normalizeToElementConfig( config.view, 'container' );
- return dispatcher => {
- dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
- if ( config.triggerBy ) {
- if ( config.triggerBy.attributes ) {
- for ( const attributeKey of config.triggerBy.attributes ) {
- dispatcher._mapReconversionTriggerEvent( config.model, `attribute:${ attributeKey }:${ config.model }` );
- }
- }
- if ( config.triggerBy.children ) {
- for ( const childName of config.triggerBy.children ) {
- dispatcher._mapReconversionTriggerEvent( config.model, `insert:${ childName }` );
- dispatcher._mapReconversionTriggerEvent( config.model, `remove:${ childName }` );
- }
- }
- }
- };
- }
- // Model attribute to view element conversion helper.
- //
- // See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
- //
- // @param {Object} config Conversion configuration.
- // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
- // of `String`s with possible values if the model attribute is an enumerable.
- // @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
- // that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
- // as parameters and returns a view attribute element. If `config.model.values` is
- // given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
- // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- // @returns {Function} Conversion helper.
- function downcastAttributeToElement( config ) {
- config = cloneDeep( config );
- const modelKey = config.model.key ? config.model.key : config.model;
- let eventName = 'attribute:' + modelKey;
- if ( config.model.name ) {
- eventName += ':' + config.model.name;
- }
- if ( config.model.values ) {
- for ( const modelValue of config.model.values ) {
- config.view[ modelValue ] = normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
- }
- } else {
- config.view = normalizeToElementConfig( config.view, 'attribute' );
- }
- const elementCreator = getFromAttributeCreator( config );
- return dispatcher => {
- dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } );
- };
- }
- // Model attribute to view attribute conversion helper.
- //
- // See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
- //
- // @param {Object} config Conversion configuration.
- // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
- // the attribute key, possible values and, optionally, an element name to convert from.
- // @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
- // the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
- // array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
- // If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
- // `{ key, value }` objects or a functions.
- // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- // @returns {Function} Conversion helper.
- function downcastAttributeToAttribute( config ) {
- config = cloneDeep( config );
- const modelKey = config.model.key ? config.model.key : config.model;
- let eventName = 'attribute:' + modelKey;
- if ( config.model.name ) {
- eventName += ':' + config.model.name;
- }
- if ( config.model.values ) {
- for ( const modelValue of config.model.values ) {
- config.view[ modelValue ] = normalizeToAttributeConfig( config.view[ modelValue ] );
- }
- } else {
- config.view = normalizeToAttributeConfig( config.view );
- }
- const elementCreator = getFromAttributeCreator( config );
- return dispatcher => {
- dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } );
- };
- }
- // Model marker to view element conversion helper.
- //
- // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
- //
- // @param {Object} config Conversion configuration.
- // @param {String} config.model The name of the model marker (or model marker group) to convert.
- // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
- // that takes the model marker data as a parameter and returns a view UI element.
- // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- // @returns {Function} Conversion helper.
- function downcastMarkerToElement( config ) {
- config = cloneDeep( config );
- config.view = normalizeToElementConfig( config.view, 'ui' );
- return dispatcher => {
- dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
- dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
- };
- }
- // Model marker to view data conversion helper.
- //
- // See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
- //
- // @param {Object} config
- // @param {String} config.model
- // @param {Function} [config.view]
- // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
- // @returns {Function} Conversion helper.
- function downcastMarkerToData( config ) {
- config = cloneDeep( config );
- const group = config.model;
- // Default conversion.
- if ( !config.view ) {
- config.view = markerName => ( {
- group,
- name: markerName.substr( config.model.length + 1 )
- } );
- }
- return dispatcher => {
- dispatcher.on( 'addMarker:' + group, insertMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
- dispatcher.on( 'removeMarker:' + group, removeMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
- };
- }
- // Model marker to highlight conversion helper.
- //
- // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
- //
- // @param {Object} config Conversion configuration.
- // @param {String} config.model The name of the model marker (or model marker group) to convert.
- // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
- // that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
- // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- // @returns {Function} Conversion helper.
- function downcastMarkerToHighlight( config ) {
- return dispatcher => {
- dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } );
- dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } );
- dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } );
- };
- }
- // Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
- // to a function (because lower level converters accept only element creator functions).
- //
- // @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
- // @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
- // @returns {Function} Element creator function to use in lower level converters.
- function normalizeToElementConfig( view, viewElementType ) {
- if ( typeof view == 'function' ) {
- // If `view` is already a function, don't do anything.
- return view;
- }
- return ( modelData, conversionApi ) => createViewElementFromDefinition( view, conversionApi, viewElementType );
- }
- // Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
- //
- // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
- // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
- // @param {'container'|'attribute'|'ui'} viewElementType
- // @returns {module:engine/view/element~Element}
- function createViewElementFromDefinition( viewElementDefinition, conversionApi, viewElementType ) {
- if ( typeof viewElementDefinition == 'string' ) {
- // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
- viewElementDefinition = { name: viewElementDefinition };
- }
- let element;
- const viewWriter = conversionApi.writer;
- const attributes = Object.assign( {}, viewElementDefinition.attributes );
- if ( viewElementType == 'container' ) {
- element = viewWriter.createContainerElement( viewElementDefinition.name, attributes );
- } else if ( viewElementType == 'attribute' ) {
- const options = {
- priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
- };
- element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options );
- } else {
- // 'ui'.
- element = viewWriter.createUIElement( viewElementDefinition.name, attributes );
- }
- if ( viewElementDefinition.styles ) {
- const keys = Object.keys( viewElementDefinition.styles );
- for ( const key of keys ) {
- viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
- }
- }
- if ( viewElementDefinition.classes ) {
- const classes = viewElementDefinition.classes;
- if ( typeof classes == 'string' ) {
- viewWriter.addClass( classes, element );
- } else {
- for ( const className of classes ) {
- viewWriter.addClass( className, element );
- }
- }
- }
- return element;
- }
- function getFromAttributeCreator( config ) {
- if ( config.model.values ) {
- return ( modelAttributeValue, conversionApi ) => {
- const view = config.view[ modelAttributeValue ];
- if ( view ) {
- return view( modelAttributeValue, conversionApi );
- }
- return null;
- };
- } else {
- return config.view;
- }
- }
- // Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
- // for generating a view attribute.
- //
- // @param {Object} view View configuration.
- function normalizeToAttributeConfig( view ) {
- if ( typeof view == 'string' ) {
- return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
- } else if ( typeof view == 'object' ) {
- // { key, value, ... }
- if ( view.value ) {
- return () => view;
- }
- // { key, ... }
- else {
- return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
- }
- } else {
- // function.
- return view;
- }
- }
- // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
- function prepareDescriptor( highlightDescriptor, data, conversionApi ) {
- // If passed descriptor is a creator function, call it. If not, just use passed value.
- const descriptor = typeof highlightDescriptor == 'function' ?
- highlightDescriptor( data, conversionApi ) :
- highlightDescriptor;
- if ( !descriptor ) {
- return null;
- }
- // Apply default descriptor priority.
- if ( !descriptor.priority ) {
- descriptor.priority = 10;
- }
- // Default descriptor id is marker name.
- if ( !descriptor.id ) {
- descriptor.id = data.markerName;
- }
- return descriptor;
- }
- /**
- * An object describing how the marker highlight should be represented in the view.
- *
- * Each text node contained in a highlighted range will be wrapped in a `<span>`
- * {@link module:engine/view/attributeelement~AttributeElement view attribute element} with CSS class(es), attributes and a priority
- * described by this object.
- *
- * Additionally, each {@link module:engine/view/containerelement~ContainerElement container element} can handle displaying the highlight
- * separately by providing the `addHighlight` and `removeHighlight` custom properties. In this case:
- *
- * * The `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
- * the element.
- * * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the
- * given ID from the element.
- *
- * @typedef {Object} module:engine/conversion/downcasthelpers~HighlightDescriptor
- *
- * @property {String|Array.<String>} classes A CSS class or an array of classes to set. If the descriptor is used to
- * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set
- * on that attribute element. If the descriptor is applied to an element, usually these classes will be set on that element, however,
- * this depends on how the element converts the descriptor.
- *
- * @property {String} [id] Descriptor identifier. If not provided, it defaults to the converted marker's name.
- *
- * @property {Number} [priority] Descriptor priority. If not provided, it defaults to `10`. If the descriptor is used to create
- * an {@link module:engine/view/attributeelement~AttributeElement attribute element}, it will be that element's
- * {@link module:engine/view/attributeelement~AttributeElement#priority priority}. If the descriptor is applied to an element,
- * the priority will be used to determine which descriptor is more important.
- *
- * @property {Object} [attributes] Attributes to set. If the descriptor is used to create
- * an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these attributes will be set on that
- * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
- * this depends on how the element converts the descriptor.
- */
|