|
|
@@ -3,17 +3,16 @@
|
|
|
* For licensing, see LICENSE.md.
|
|
|
*/
|
|
|
|
|
|
+import ModelRange from '../model/range';
|
|
|
+
|
|
|
import ViewElement from '../view/element';
|
|
|
import ViewAttributeElement from '../view/attributeelement';
|
|
|
import ViewText from '../view/text';
|
|
|
import ViewRange from '../view/range';
|
|
|
-import ViewPosition from '../view/position';
|
|
|
-import ViewTreeWalker from '../view/treewalker';
|
|
|
import viewWriter from '../view/writer';
|
|
|
-import ModelRange from '../model/range';
|
|
|
|
|
|
/**
|
|
|
- * Contains {@link module:engine/model/model model} to {@link module:engine/view/view view} converters for
|
|
|
+ * Contains model to view converters for
|
|
|
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
|
|
|
*
|
|
|
* @module engine/conversion/model-to-view-converters
|
|
|
@@ -92,6 +91,34 @@ export function insertText() {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * Function factory, creates a default model-to-view 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, true );
|
|
|
+
|
|
|
+ const viewRange = new ViewRange( viewStart, viewEnd );
|
|
|
+
|
|
|
+ // Trim the range to remove in case some UI elements are on the view range boundaries.
|
|
|
+ const removed = viewWriter.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 ViewRange.createIn( removed ).getItems() ) {
|
|
|
+ conversionApi.mapper.unbindViewElement( child );
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
* Function factory, creates a converter that converts marker adding change to the view ui element.
|
|
|
* The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
|
|
|
* In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
|
|
|
@@ -108,15 +135,17 @@ export function insertUIElement( elementCreator ) {
|
|
|
return ( evt, data, consumable, conversionApi ) => {
|
|
|
let viewStartElement, viewEndElement;
|
|
|
|
|
|
+ // 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.
|
|
|
if ( elementCreator instanceof ViewElement ) {
|
|
|
viewStartElement = elementCreator.clone( true );
|
|
|
viewEndElement = elementCreator.clone( true );
|
|
|
} else {
|
|
|
data.isOpening = true;
|
|
|
- viewStartElement = elementCreator( data, consumable, conversionApi );
|
|
|
+ viewStartElement = elementCreator( data, conversionApi );
|
|
|
|
|
|
data.isOpening = false;
|
|
|
- viewEndElement = elementCreator( data, consumable, conversionApi );
|
|
|
+ viewEndElement = elementCreator( data, conversionApi );
|
|
|
}
|
|
|
|
|
|
if ( !viewStartElement || !viewEndElement ) {
|
|
|
@@ -124,91 +153,77 @@ export function insertUIElement( elementCreator ) {
|
|
|
}
|
|
|
|
|
|
const markerRange = data.markerRange;
|
|
|
- const eventName = evt.name;
|
|
|
-
|
|
|
- // Marker that is collapsed has consumable build differently that non-collapsed one.
|
|
|
- // For more information see `addMarker` and `removeMarker` events description.
|
|
|
- // If marker's range is collapsed - check if it can be consumed.
|
|
|
- if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // If marker's range is not collapsed - consume all items inside.
|
|
|
- for ( const value of markerRange ) {
|
|
|
- if ( !consumable.consume( value.item, eventName ) ) {
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
const mapper = conversionApi.mapper;
|
|
|
|
|
|
+ // Add "opening" element.
|
|
|
viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
|
|
|
|
|
|
+ // Add "closing" element only if range is not collapsed.
|
|
|
if ( !markerRange.isCollapsed ) {
|
|
|
viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
|
|
|
}
|
|
|
+
|
|
|
+ evt.stop();
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
|
|
|
- * from 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. If not provided, model attributes will be converted to view elements attributes
|
|
|
- * on 1-to-1 basis.
|
|
|
- *
|
|
|
- * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
|
|
|
- *
|
|
|
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
|
|
|
- *
|
|
|
- * modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
|
|
|
- * // Change attribute key from `customAttr` to `class` in 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 key-value pair.
|
|
|
- * return { key, value };
|
|
|
- * } ) );
|
|
|
+ * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
|
|
|
+ * basing on marker remove change.
|
|
|
*
|
|
|
- * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
|
|
|
- * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
|
|
|
- * The function is passed all the parameters of the
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute}
|
|
|
- * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute} event.
|
|
|
- * @returns {Function} Set/change attribute converter.
|
|
|
+ * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
|
|
|
+ * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
|
|
|
+ * @returns {Function} Remove ui element converter.
|
|
|
*/
|
|
|
-export function setAttribute( attributeCreator ) {
|
|
|
- attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
|
|
|
+export function removeUIElement( elementCreator ) {
|
|
|
+ return ( evt, data, conversionApi ) => {
|
|
|
+ let viewStartElement, viewEndElement;
|
|
|
|
|
|
- return ( evt, data, consumable, conversionApi ) => {
|
|
|
- if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
|
|
|
+ // Create two view elements. One will be used to remove "opening element", the other for "closing element".
|
|
|
+ // If marker was collapsed, only "opening" element will be removed.
|
|
|
+ if ( elementCreator instanceof ViewElement ) {
|
|
|
+ viewStartElement = elementCreator.clone( true );
|
|
|
+ viewEndElement = elementCreator.clone( true );
|
|
|
+ } else {
|
|
|
+ data.isOpening = true;
|
|
|
+ viewStartElement = elementCreator( data, conversionApi );
|
|
|
+
|
|
|
+ data.isOpening = false;
|
|
|
+ viewEndElement = elementCreator( data, conversionApi );
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( !viewStartElement || !viewEndElement ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
|
|
|
+ const markerRange = data.markerRange;
|
|
|
+
|
|
|
+ // When removing the ui elements, we map the model range to view twice, because that view range
|
|
|
+ // may change after the first clearing.
|
|
|
+ if ( !markerRange.isCollapsed ) {
|
|
|
+ viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove "opening" element.
|
|
|
+ viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
|
|
|
|
|
|
- conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
|
|
|
+ evt.stop();
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes
|
|
|
- * that were converted to the view element attributes in the view. You may provide a custom function to generate a
|
|
|
- * key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
|
|
|
+ * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
|
|
|
*
|
|
|
- * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
|
|
|
+ * Attributes from 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 elements
|
|
|
+ * attributes on 1-to-1 basis.
|
|
|
*
|
|
|
- * **Note:** You can use the same attribute creator as in {@link module:engine/conversion/model-to-view-converters~setAttribute}.
|
|
|
+ * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
|
|
|
*
|
|
|
* The converter automatically consumes corresponding value from consumables list and stops the event (see
|
|
|
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
|
|
|
*
|
|
|
- * modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
|
|
|
+ * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( data ) => {
|
|
|
* // Change attribute key from `customAttr` to `class` in view.
|
|
|
* const key = 'class';
|
|
|
* let value = data.attributeNewValue;
|
|
|
@@ -223,31 +238,35 @@ export function setAttribute( attributeCreator ) {
|
|
|
* } ) );
|
|
|
*
|
|
|
* @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
|
|
|
- * represents attribute key and attribute value to be removed from {@link module:engine/view/element~Element view element}.
|
|
|
+ * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
|
|
|
* The function is passed all the parameters of the
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}
|
|
|
- * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}.
|
|
|
- * @returns {Function} Remove attribute converter.
|
|
|
+ * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute} event.
|
|
|
+ * @returns {Function} Set/change attribute converter.
|
|
|
*/
|
|
|
-export function removeAttribute( attributeCreator ) {
|
|
|
- attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) );
|
|
|
+export function changeAttribute( attributeCreator ) {
|
|
|
+ attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
|
|
|
|
|
|
return ( evt, data, consumable, conversionApi ) => {
|
|
|
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi );
|
|
|
+ const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
|
|
|
|
|
|
- conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
|
|
|
+ if ( data.attributeNewValue !== null ) {
|
|
|
+ conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
|
|
|
+ } else {
|
|
|
+ conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
|
|
|
+ }
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
|
|
|
- * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
|
|
|
- * the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are
|
|
|
- * represented as an element in the view:
|
|
|
+ * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
|
|
|
+ *
|
|
|
+ * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to
|
|
|
+ * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model
|
|
|
+ * but are represented as an element in the view:
|
|
|
*
|
|
|
* [paragraph] MODEL ====> VIEW <p>
|
|
|
* |- a {bold: true} |- <b>
|
|
|
@@ -256,7 +275,7 @@ export function removeAttribute( attributeCreator ) {
|
|
|
*
|
|
|
* The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
|
|
|
* the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
|
|
|
+ * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute event}.
|
|
|
* It's expected that the function returns a {@link module:engine/view/element~Element}.
|
|
|
* The result of the function will be the wrapping element.
|
|
|
* When provided `Function` does not return element, then will be no conversion.
|
|
|
@@ -264,19 +283,26 @@ export function removeAttribute( attributeCreator ) {
|
|
|
* The converter automatically consumes corresponding value from consumables list, stops the event (see
|
|
|
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
|
|
|
*
|
|
|
- * modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
|
|
|
+ * modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
|
|
|
*
|
|
|
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
|
|
|
* be used for wrapping.
|
|
|
* @returns {Function} Set/change attribute converter.
|
|
|
*/
|
|
|
-export function wrapItem( elementCreator ) {
|
|
|
+export function wrap( elementCreator ) {
|
|
|
return ( evt, data, consumable, conversionApi ) => {
|
|
|
- const viewElement = ( elementCreator instanceof ViewElement ) ?
|
|
|
+ // 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 instanceof ViewElement ) ?
|
|
|
+ elementCreator.clone( true ) :
|
|
|
+ elementCreator( data.attributeOldValue, data, consumable, conversionApi );
|
|
|
+
|
|
|
+ // Create node to wrap with.
|
|
|
+ const newViewElement = ( elementCreator instanceof ViewElement ) ?
|
|
|
elementCreator.clone( true ) :
|
|
|
elementCreator( data.attributeNewValue, data, consumable, conversionApi );
|
|
|
|
|
|
- if ( !viewElement ) {
|
|
|
+ if ( !oldViewElement && !newViewElement ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -286,167 +312,61 @@ export function wrapItem( elementCreator ) {
|
|
|
|
|
|
let viewRange = conversionApi.mapper.toViewRange( data.range );
|
|
|
|
|
|
- // If this is a change event (because old value is not empty) and the creator is a function (so
|
|
|
- // it may create different view elements basing on attribute value) we have to create
|
|
|
- // view element basing on old value and unwrap it before wrapping with a newly created view element.
|
|
|
- if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
|
|
|
- const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
|
|
|
+ // First, unwrap the range from current wrapper.
|
|
|
+ if ( data.attributeOldValue !== null ) {
|
|
|
viewRange = viewWriter.unwrap( viewRange, oldViewElement );
|
|
|
}
|
|
|
|
|
|
- viewWriter.wrap( viewRange, viewElement );
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
|
|
|
- * attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
|
|
|
- * view element if given attribute was removed.
|
|
|
- *
|
|
|
- * The view element type that will be unwrapped depends on passed parameter.
|
|
|
- * If {@link module:engine/view/element~Element} was passed, it will be used to look for similar element in the view for unwrapping.
|
|
|
- * If `Function` is provided, it is passed all the parameters of the
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
|
|
|
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
|
|
|
- * The result of the function will be used to look for similar element in the view for unwrapping.
|
|
|
- *
|
|
|
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
|
|
|
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
|
|
|
- *
|
|
|
- * modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
|
|
|
- *
|
|
|
- * @see module:engine/conversion/model-to-view-converters~wrapItem
|
|
|
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
|
|
|
- * be used for unwrapping.
|
|
|
- * @returns {Function} Remove attribute converter.
|
|
|
- */
|
|
|
-export function unwrapItem( elementCreator ) {
|
|
|
- return ( evt, data, consumable, conversionApi ) => {
|
|
|
- const viewElement = ( elementCreator instanceof ViewElement ) ?
|
|
|
- elementCreator.clone( true ) :
|
|
|
- elementCreator( data.attributeOldValue, data, consumable, conversionApi );
|
|
|
-
|
|
|
- if ( !viewElement ) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
|
|
|
- return;
|
|
|
+ // Then wrap with the new wrapper.
|
|
|
+ if ( data.attributeNewValue !== null ) {
|
|
|
+ viewWriter.wrap( viewRange, newViewElement );
|
|
|
}
|
|
|
-
|
|
|
- const viewRange = conversionApi.mapper.toViewRange( data.range );
|
|
|
-
|
|
|
- viewWriter.unwrap( viewRange, viewElement );
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Function factory, creates a default model-to-view converter for node remove changes.
|
|
|
- *
|
|
|
- * modelDispatcher.on( 'remove', remove() );
|
|
|
- *
|
|
|
- * @returns {Function} Remove event converter.
|
|
|
- */
|
|
|
-export function remove() {
|
|
|
- return ( evt, data, consumable, conversionApi ) => {
|
|
|
- if ( !consumable.consume( data.item, 'remove' ) ) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // We cannot map non-existing positions from model to view. Since a range was removed
|
|
|
- // from the model, we cannot recreate that range and map it to view, because
|
|
|
- // end of that range is incorrect.
|
|
|
- // Instead we will use `data.sourcePosition` as this is the last correct model position and
|
|
|
- // it is a position before the removed item. Then, we will calculate view range to remove "manually".
|
|
|
- let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
|
|
|
- let viewRange;
|
|
|
-
|
|
|
- if ( data.item.is( 'element' ) ) {
|
|
|
- // Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
|
|
|
- // already mapped to another element (this happens when move change is converted).
|
|
|
- // In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
|
|
|
- //
|
|
|
- // First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition`
|
|
|
- // may be before a ui element, before attribute element or at the end of text element.
|
|
|
- viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) );
|
|
|
-
|
|
|
- if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) {
|
|
|
- viewPosition = ViewPosition.createAfter( viewPosition.parent );
|
|
|
- }
|
|
|
-
|
|
|
- viewRange = ViewRange.createOn( viewPosition.nodeAfter );
|
|
|
- } else {
|
|
|
- // If removed item is a text node, we need to traverse view tree to find the view range to remove.
|
|
|
- // Range to remove will start `viewPosition` and should contain amount of characters equal to the amount of removed characters.
|
|
|
- const viewRangeEnd = _shiftViewPositionByCharacters( viewPosition, data.item.offsetSize );
|
|
|
- viewRange = new ViewRange( viewPosition, viewRangeEnd );
|
|
|
- }
|
|
|
-
|
|
|
- // Trim the range to remove in case some UI elements are on the view range boundaries.
|
|
|
- viewWriter.remove( viewRange.getTrimmed() );
|
|
|
-
|
|
|
- // Unbind this element only if it was moved to graveyard.
|
|
|
- // The dispatcher#remove event will also be fired if the element was moved to another place (remove+insert are fired).
|
|
|
- // Let's say that <b> is moved before <a>. The view will be changed like this:
|
|
|
- //
|
|
|
- // 1) start: <a></a><b></b>
|
|
|
- // 2) insert: <b (new)></b><a></a><b></b>
|
|
|
- // 3) remove: <b (new)></b><a></a>
|
|
|
- //
|
|
|
- // If we'll unbind the <b> element in step 3 we'll also lose binding of the <b (new)> element in the view,
|
|
|
- // because unbindModelElement() cancels both bindings – (model <b> => view <b (new)>) and (view <b (new)> => model <b>).
|
|
|
- // We can't lose any of these.
|
|
|
- //
|
|
|
- // See #847.
|
|
|
- if ( data.item.root.rootName == '$graveyard' ) {
|
|
|
- conversionApi.mapper.unbindModelElement( data.item );
|
|
|
- }
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with
|
|
|
+ * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with
|
|
|
* {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
|
|
|
- * See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
|
|
|
+ * See {link module:engine/conversion/model-to-view-converters~createViewElementFromHighlightDescriptor}.
|
|
|
*
|
|
|
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
|
|
|
* @return {Function}
|
|
|
*/
|
|
|
export function highlightText( highlightDescriptor ) {
|
|
|
return ( evt, data, consumable, conversionApi ) => {
|
|
|
- const descriptor = typeof highlightDescriptor == 'function' ?
|
|
|
- highlightDescriptor( data, consumable, conversionApi ) :
|
|
|
- highlightDescriptor;
|
|
|
+ if ( data.markerRange.isCollapsed ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
const modelItem = data.item;
|
|
|
|
|
|
- if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'textProxy' ) ) {
|
|
|
+ if ( !modelItem.is( 'textProxy' ) ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if ( !consumable.consume( modelItem, evt.name ) ) {
|
|
|
+ const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
|
|
|
+
|
|
|
+ if ( !descriptor ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if ( !descriptor.id ) {
|
|
|
- descriptor.id = data.markerName;
|
|
|
+ if ( !consumable.consume( modelItem, evt.name ) ) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
const viewElement = createViewElementFromHighlightDescriptor( descriptor );
|
|
|
const viewRange = conversionApi.mapper.toViewRange( data.range );
|
|
|
|
|
|
- if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) {
|
|
|
- viewWriter.wrap( viewRange, viewElement );
|
|
|
- } else {
|
|
|
- viewWriter.unwrap( viewRange, viewElement );
|
|
|
- }
|
|
|
+ viewWriter.wrap( viewRange, viewElement );
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Converter function factory. Creates a function which applies the marker's highlight to all elements inside a marker's range.
|
|
|
- * The converter checks if an element has the addHighlight and removeHighlight functions stored as
|
|
|
- * {@link module:engine/view/element~Element#setCustomProperty custom properties} and if so use them to apply the highlight.
|
|
|
+ * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
|
|
|
+ *
|
|
|
+ * The converter checks if an element has `addHighlight` function stored as
|
|
|
+ * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight.
|
|
|
+ *
|
|
|
* In such case converter will consume all element's children, assuming that they were handled by element itself.
|
|
|
* If the highlight descriptor will not provide priority, priority `10` will be used as default, to be compliant with
|
|
|
* {@link module:engine/conversion/model-to-view-converters~highlightText} method which uses default priority of
|
|
|
@@ -461,33 +381,29 @@ export function highlightText( highlightDescriptor ) {
|
|
|
*/
|
|
|
export function highlightElement( highlightDescriptor ) {
|
|
|
return ( evt, data, consumable, conversionApi ) => {
|
|
|
- const descriptor = typeof highlightDescriptor == 'function' ?
|
|
|
- highlightDescriptor( data, consumable, conversionApi ) :
|
|
|
- highlightDescriptor;
|
|
|
+ if ( data.markerRange.isCollapsed ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
const modelItem = data.item;
|
|
|
|
|
|
- if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'element' ) ) {
|
|
|
+ if ( !modelItem.is( 'element' ) ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if ( !consumable.test( data.item, evt.name ) ) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
|
|
|
|
|
|
- if ( !descriptor.priority ) {
|
|
|
- descriptor.priority = 10;
|
|
|
+ if ( !descriptor ) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- if ( !descriptor.id ) {
|
|
|
- descriptor.id = data.markerName;
|
|
|
+ if ( !consumable.test( modelItem, evt.name ) ) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
const viewElement = conversionApi.mapper.toViewElement( modelItem );
|
|
|
- const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker';
|
|
|
- const highlightHandlingMethod = addMarker ? 'addHighlight' : 'removeHighlight';
|
|
|
|
|
|
- if ( viewElement && viewElement.getCustomProperty( highlightHandlingMethod ) ) {
|
|
|
+ if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
|
|
|
// Consume element itself.
|
|
|
consumable.consume( data.item, evt.name );
|
|
|
|
|
|
@@ -496,65 +412,98 @@ export function highlightElement( highlightDescriptor ) {
|
|
|
consumable.consume( value.item, evt.name );
|
|
|
}
|
|
|
|
|
|
- viewElement.getCustomProperty( highlightHandlingMethod )( viewElement, addMarker ? descriptor : descriptor.id );
|
|
|
+ viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor );
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
|
|
|
- * basing on marker remove change.
|
|
|
+ * Function factory, creates a converter that converts model marker add/change/remove to the view.
|
|
|
*
|
|
|
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
|
|
|
- * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
|
|
|
- * @returns {Function} Remove ui element converter.
|
|
|
+ * The result of this conversion is different for text nodes and elements.
|
|
|
+ *
|
|
|
+ * Text nodes are wrapped with {@link module:engine/view/attributeelement~AttributeElement} created from provided
|
|
|
+ * highlight descriptor. See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
|
|
|
+ *
|
|
|
+ * For elements, the converter checks if an element has `addHighlight` and `removeHighlight` functions stored as
|
|
|
+ * {@link module:engine/view/element~Element#setCustomProperty custom properties}. If so, it uses them to apply the highlight.
|
|
|
+ * In such case, children of that element will not be converted. When `addHighlight` and `removeHighlight` are not present,
|
|
|
+ * element is not converted in any special way, instead converter will proceed to convert element's child nodes. Most
|
|
|
+ * common case is that the element will be given a special class, style or attribute basing on the descriptor.
|
|
|
+ *
|
|
|
+ * If the highlight descriptor will not provide `priority` property, `10` will be used.
|
|
|
+ *
|
|
|
+ * If the highlight descriptor will not provide `id` property, name of the marker will be used.
|
|
|
+ *
|
|
|
+ * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
|
|
|
+ * @return {Function}
|
|
|
*/
|
|
|
-export function removeUIElement( elementCreator ) {
|
|
|
- return ( evt, data, consumable, conversionApi ) => {
|
|
|
- let viewStartElement, viewEndElement;
|
|
|
-
|
|
|
- if ( elementCreator instanceof ViewElement ) {
|
|
|
- viewStartElement = elementCreator.clone( true );
|
|
|
- viewEndElement = elementCreator.clone( true );
|
|
|
- } else {
|
|
|
- data.isOpening = true;
|
|
|
- viewStartElement = elementCreator( data, consumable, conversionApi );
|
|
|
-
|
|
|
- data.isOpening = false;
|
|
|
- viewEndElement = elementCreator( data, consumable, conversionApi );
|
|
|
- }
|
|
|
-
|
|
|
- if ( !viewStartElement || !viewEndElement ) {
|
|
|
+export function removeHighlight( highlightDescriptor ) {
|
|
|
+ return ( evt, data, conversionApi ) => {
|
|
|
+ // This conversion makes sense only for non-collapsed range.
|
|
|
+ if ( data.markerRange.isCollapsed ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const markerRange = data.markerRange;
|
|
|
- const eventName = evt.name;
|
|
|
+ const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
|
|
|
|
|
|
- // If marker's range is collapsed - check if it can be consumed.
|
|
|
- if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
|
|
|
+ if ( !descriptor ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Check if all items in the range can be consumed, and consume them.
|
|
|
- for ( const value of markerRange ) {
|
|
|
- if ( !consumable.consume( value.item, eventName ) ) {
|
|
|
- return;
|
|
|
+ const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
|
|
|
+
|
|
|
+ // Retrieve all items in the affected range. We will process them and remove highlight from them appropriately.
|
|
|
+ const items = new Set( viewRange.getItems() );
|
|
|
+
|
|
|
+ // First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
|
|
|
+ for ( const item of items ) {
|
|
|
+ if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
|
|
|
+ item.getCustomProperty( 'removeHighlight' )( item, descriptor.id );
|
|
|
+
|
|
|
+ // If container element had custom handling, remove all it's children from further processing.
|
|
|
+ for ( const descendant of ViewRange.createIn( item ) ) {
|
|
|
+ items.delete( descendant );
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const viewRange = conversionApi.mapper.toViewRange( markerRange );
|
|
|
-
|
|
|
- // First remove closing element.
|
|
|
- viewWriter.clear( viewRange.getEnlarged(), viewEndElement );
|
|
|
+ // Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
|
|
|
+ // to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
|
|
|
+ const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
|
|
|
|
|
|
- // If closing and opening elements are not the same then remove opening element.
|
|
|
- if ( !viewStartElement.isSimilar( viewEndElement ) ) {
|
|
|
- viewWriter.clear( viewRange.getEnlarged(), viewStartElement );
|
|
|
+ for ( const item of Array.from( items ).reverse() ) {
|
|
|
+ if ( item.is( 'textProxy' ) ) {
|
|
|
+ viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement );
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+// 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;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Returns the consumable type that is to be consumed in an event, basing on that event name.
|
|
|
*
|
|
|
@@ -567,26 +516,6 @@ export function eventNameToConsumableType( evtName ) {
|
|
|
return parts[ 0 ] + ':' + parts[ 1 ];
|
|
|
}
|
|
|
|
|
|
-// Helper function that shifts given view `position` in a way that returned position is after `howMany` characters compared
|
|
|
-// to the original `position`.
|
|
|
-// Because in view there might be view ui elements splitting text nodes, we cannot simply use `ViewPosition#getShiftedBy()`.
|
|
|
-function _shiftViewPositionByCharacters( position, howMany ) {
|
|
|
- // Create a walker that will walk the view tree starting from given position and walking characters one-by-one.
|
|
|
- const walker = new ViewTreeWalker( { startPosition: position, singleCharacters: true } );
|
|
|
- // We will count visited characters and return the position after `howMany` characters.
|
|
|
- let charactersFound = 0;
|
|
|
-
|
|
|
- for ( const value of walker ) {
|
|
|
- if ( value.type == 'text' ) {
|
|
|
- charactersFound++;
|
|
|
-
|
|
|
- if ( charactersFound == howMany ) {
|
|
|
- return walker.position;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
|
|
|
* provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority
|