Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit d2e9f06

Browse files
authoredDec 21, 2017
Merge pull request #1205 from ckeditor/t/1198
Feature: Introduced `ViewElementDefinition` and `definition-based-converters` module with a set of utils allowing to turn element definitions to converters. Closes #1198.
2 parents 79f621f + 4760cce commit d2e9f06

File tree

3 files changed

+867
-0
lines changed

3 files changed

+867
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module engine/conversion/definition-based-converters
8+
*/
9+
10+
import AttributeElement from '../view/attributeelement';
11+
import ViewContainerElement from '../view/containerelement';
12+
13+
import buildModelConverter from './buildmodelconverter';
14+
import buildViewConverter from './buildviewconverter';
15+
16+
/**
17+
* Helper for creating a model element to {@link module:engine/view/containerelement~ContainerElement view container element}
18+
* converters.
19+
*
20+
* You can create a converter by using a simplified converter definition:
21+
*
22+
* modelElementToViewContainerElement( {
23+
* model: 'heading1',
24+
* view: 'h1',
25+
* }, [ editor.editing.modelToView, editor.data.modelToView ] );
26+
*
27+
* Or by using a full-flavored view object:
28+
*
29+
* modelElementToViewContainerElement( {
30+
* model: 'heading1',
31+
* view: {
32+
* name: 'h1',
33+
* class: [ 'header', 'article-header' ],
34+
* attribute: {
35+
* data-header: 'level-1',
36+
* }
37+
* },
38+
* }, [ editor.editing.modelToView, editor.data.modelToView ] );
39+
*
40+
* The above will generate the following view element:
41+
*
42+
* <h1 class="header article-header" data-header="level-1">...</h1>
43+
*
44+
* @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition The converter configuration.
45+
* @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
46+
*/
47+
export function modelElementToViewContainerElement( definition, dispatchers ) {
48+
const { model: modelElement, targetView } = normalizeConverterDefinition( definition );
49+
50+
buildModelConverter()
51+
.for( ...dispatchers )
52+
.fromElement( modelElement )
53+
.toElement( () => createViewElementFromDefinition( targetView, ViewContainerElement ) );
54+
}
55+
56+
/**
57+
* Helper for creating a view element to model element converters.
58+
*
59+
* Besides converting the view element specified in the `view` property it will also convert all view elements
60+
* which match the patterns defined in the `acceptAlso` property. Such a "wide" converters are often needed so the editor
61+
* is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
62+
* "flavors" of the same editing features (e.g. you may want to handle `<h1>` and `<p class="heading1">` as headings).
63+
*
64+
* The `model` property defines the model element name to be used by the converter.
65+
*
66+
* A converter can be defined using a simplified converter definition:
67+
*
68+
* viewToModelElement( { model: 'heading1', view: 'h1' }, [ dispatcher ] );
69+
*
70+
* Or by using a full-flavored definition:
71+
*
72+
* viewToModelElement( {
73+
* model: 'heading1',
74+
* view: {
75+
* name: 'p',
76+
* attribute: {
77+
* 'data-heading': 'true'
78+
* },
79+
* // You may need to use a high-priority listener to catch elements
80+
* // which are handled by other (usually – more generic) converters too.
81+
* priority: 'high'
82+
* }
83+
* }, [ editor.data.viewToModel ] );
84+
*
85+
* Or with the `acceptAlso` property to match more patterns:
86+
*
87+
* viewToModelElement( {
88+
* model: 'heading1',
89+
* view: 'h1',
90+
* acceptAlso: [
91+
* { name: 'p', attribute: { 'data-heading': 'level1' }, priority: 'high' },
92+
* { name: 'h2', class: 'heading-main' },
93+
* { name: 'div', style: { 'font-weight': 'bold', font-size: '24px' } }
94+
* ]
95+
* }, [ editor.data.viewToModel ] );
96+
*
97+
* The above example will convert an existing view elements:
98+
*
99+
* <h1>A heading</h1>
100+
* <h2 class="heading-main">Another heading</h2>
101+
* <p data-heading="level1">Paragraph-like heading</p>
102+
* <div style="font-size:24px; font-weigh:bold;">Another non-semantic header</div>
103+
*
104+
* into `heading1` model elements so in model it will be represented as:
105+
*
106+
* <heading1>A heading</heading1>
107+
* <heading1>Another heading</heading1>
108+
* <heading1>Paragraph-like heading</heading1>
109+
* <heading1>Another non-semantic header</heading1>
110+
*
111+
* @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
112+
* @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
113+
*/
114+
export function viewToModelElement( definition, dispatchers ) {
115+
const { model: modelElement, sourceViews } = normalizeConverterDefinition( definition );
116+
117+
const converter = prepareViewConverter( dispatchers, sourceViews );
118+
119+
converter.toElement( modelElement );
120+
}
121+
122+
/**
123+
* Helper for creating a model attribute to {@link module:engine/view/attributeelement~AttributeElement view attribute element}
124+
* converters.
125+
*
126+
* A converter can be defined by using a simplified converter definition:
127+
*
128+
* modelAttributeToViewAttributeElement( 'bold', [
129+
* {
130+
* model: 'true',
131+
* view: 'strong'
132+
* }
133+
* ], [ editor.editing.modelToView, editor.data.modelToView ] );
134+
*
135+
* Or defining full-flavored view objects:
136+
*
137+
* modelAttributeToViewAttributeElement( 'fontSize', [
138+
* {
139+
* model: 'big',
140+
* view: {
141+
* name: 'span',
142+
* style: { 'font-size': '1.2em' }
143+
* },
144+
* },
145+
* {
146+
* model: 'small',
147+
* view: {
148+
* name: 'span',
149+
* style: { 'font-size': '0.8em' }
150+
* },
151+
* }
152+
* ], [ editor.editing.modelToView, editor.data.modelToView ] );
153+
*
154+
* The above will generate the following view element from a `fontSize="big"` model attribute:
155+
*
156+
* <span style="font-size: 1.2em">...</span>
157+
*
158+
* @param {String} attributeName The name of the model attribute which should be converted.
159+
* @param {Array.<module:engine/conversion/definition-based-converters~ConverterDefinition>} definitions A conversion configuration objects
160+
* for each possible attribute value.
161+
* @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
162+
*/
163+
export function modelAttributeToViewAttributeElement( attributeName, definitions, dispatchers ) {
164+
// Create a map of attributeValue - to - ViewElementDefinition.
165+
const valueToTargetViewMap = definitions
166+
.map( normalizeConverterDefinition )
167+
.reduce( ( mapObject, normalizedDefinition ) => {
168+
mapObject[ normalizedDefinition.model ] = normalizedDefinition.targetView;
169+
170+
return mapObject;
171+
}, {} );
172+
173+
buildModelConverter()
174+
.for( ...dispatchers )
175+
.fromAttribute( attributeName )
176+
.toElement( value => {
177+
const targetView = valueToTargetViewMap[ value ];
178+
179+
if ( !targetView ) {
180+
return;
181+
}
182+
183+
return createViewElementFromDefinition( targetView, AttributeElement );
184+
} );
185+
}
186+
187+
/**
188+
* Helper for creating view element to model attribute converters.
189+
*
190+
* Besides converting the view element specified in the `view` property it will also convert all view elements
191+
* which match the patterns defined in the `acceptAlso` property. Such "wide" converters are often needed so the editor
192+
* is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
193+
* "flavors" of the same editing features (e.g. bold might be represented as `<b>`, `<strong>` or
194+
* `<span style="font-weight:bold">`).
195+
*
196+
* The `model` property defines the value of the model attribute.
197+
*
198+
* A converter can be defined using a simplified converter definition:
199+
*
200+
* viewToModelAttribute( 'bold', { model: true, view: 'strong' }, [ dispatcher ] );
201+
*
202+
* Or by using a full-flavored definition:
203+
*
204+
* viewToModelAttribute( 'fontSize', {
205+
* model: 'big',
206+
* view: {
207+
* name: 'span',
208+
* style: {
209+
* 'font-size': '1.2em'
210+
* }
211+
* }
212+
* }, [ editor.data.viewToModel ] );
213+
*
214+
* Or with the `acceptAlso` property to match more patterns:
215+
*
216+
* viewToModelAttribute( 'fontSize', {
217+
* model: 'big',
218+
* view: {
219+
* name: 'span',
220+
* class: 'text-big'
221+
* },
222+
* acceptAlso: [
223+
* { name: 'span', attribute: { 'data-size': 'big' } },
224+
* { name: 'span', class: [ 'font', 'font-huge' ] },
225+
* { name: 'span', style: { font-size: '18px' } }
226+
* ]
227+
* }, [ editor.data.viewToModel ] );
228+
*
229+
* The above example will convert the following view elements:
230+
*
231+
* <p>
232+
* An example text with some big elements:
233+
* <span class="text-big">one</span>,
234+
* <span data-size="big">two</span>,
235+
* <span class="font font-huge">three</span>,
236+
* <span style="font-size: 18px">four</span>
237+
* </p>
238+
*
239+
* to a `fontSize="big"` model attribute:
240+
*
241+
* <paragraph>
242+
* An example text with some big elements:
243+
* <$text fontSize="big">one</$text>,
244+
* <$text fontSize="big">two</$text>,
245+
* <$text fontSize="big">three</$text>,
246+
* <$text fontSize="big">four</$text>
247+
* </paragraph>
248+
*
249+
* @param {String} attributeName Attribute name to which convert.
250+
* @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
251+
* @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
252+
*/
253+
export function viewToModelAttribute( attributeName, definition, dispatchers ) {
254+
const { model: attributeValue, sourceViews } = normalizeConverterDefinition( definition );
255+
256+
const converter = prepareViewConverter( dispatchers, sourceViews );
257+
258+
converter.toAttribute( () => ( {
259+
key: attributeName,
260+
value: attributeValue
261+
} ) );
262+
}
263+
264+
// Normalize a {@link module:engine/conversion/definition-based-converters~ConverterDefinition}
265+
// into internal object used when building converters.
266+
//
267+
// @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition An object that defines view to model
268+
// and model to view conversion.
269+
// @returns {Object}
270+
function normalizeConverterDefinition( definition ) {
271+
const model = definition.model;
272+
const view = definition.view;
273+
274+
// View definition might be defined as a name of an element.
275+
const targetView = typeof view == 'string' ? { name: view } : view;
276+
277+
const sourceViews = Array.from( definition.acceptsAlso ? definition.acceptsAlso : [] );
278+
279+
// Source views also accepts default view definition used in model-to-view conversion.
280+
sourceViews.push( targetView );
281+
282+
return { model, targetView, sourceViews };
283+
}
284+
285+
// Helper method for preparing a view converter from passed view definitions.
286+
//
287+
// @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
288+
// @param {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} viewDefinitions
289+
// @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
290+
function prepareViewConverter( dispatchers, viewDefinitions ) {
291+
const converter = buildViewConverter().for( ...dispatchers );
292+
293+
for ( const viewDefinition of viewDefinitions ) {
294+
converter.from( Object.assign( {}, viewDefinition ) );
295+
296+
if ( viewDefinition.priority ) {
297+
converter.withPriority( viewDefinition.priority );
298+
}
299+
}
300+
301+
return converter;
302+
}
303+
304+
// Creates view element instance from provided viewElementDefinition and class.
305+
//
306+
// @param {module:engine/view/viewelementdefinition~ViewElementDefinition} viewElementDefinition
307+
// @param {Function} ViewElementClass
308+
// @returns {module:engine/view/element~Element}
309+
function createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
310+
const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
311+
312+
if ( viewElementDefinition.style ) {
313+
element.setStyle( viewElementDefinition.style );
314+
}
315+
316+
const classes = viewElementDefinition.class;
317+
318+
if ( classes ) {
319+
element.addClass( ... typeof classes === 'string' ? [ classes ] : classes );
320+
}
321+
322+
return element;
323+
}
324+
325+
/**
326+
* Defines conversion details. It is used in configuration based converters:
327+
* - {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement}
328+
* - {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement}
329+
* - {@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
330+
* - {@link module:engine/conversion/definition-based-converters~viewToModelElement}
331+
*
332+
* @typedef {Object} ConverterDefinition
333+
* @property {String} model Defines to model conversion. When using to element conversion
334+
* ({@link module:engine/conversion/definition-based-converters~viewToModelElement}
335+
* and {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement})
336+
* it defines element name. When using to attribute conversion
337+
* ({@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
338+
* and {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement})
339+
* it defines attribute value to which it is converted.
340+
* @property {module:engine/view/viewelementdefinition~ViewElementDefinition} view Defines model to view conversion and is also used
341+
* in view to model conversion pipeline.
342+
* @property {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} acceptAlso An array with all matched elements that
343+
* view to model conversion should also accepts.
344+
*/

‎src/view/viewelementdefinition.jsdoc

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module engine/view/viewelementdefinition
8+
*/
9+
10+
/**
11+
* An object defining view element used in {@link module:engine/conversion/definition-based-converters} as part of
12+
* {@link module:engine/conversion/definition-based-converters~ConverterDefinition}.
13+
*
14+
* It describe a view element that is used
15+
*
16+
* const viewDefinition = {
17+
* name: 'h1',
18+
* class: [ 'foo', 'bar' ]
19+
* };
20+
*
21+
* Above describes a view element:
22+
*
23+
* <h1 class="foo bar">...</h1>
24+
*
25+
* For elements without attributes you can use shorthand string version:
26+
*
27+
* const viewDefinition = 'p';
28+
*
29+
* which will be treated as:
30+
*
31+
* const viewDefinition = {
32+
* name: 'p'
33+
* };
34+
*
35+
* @typedef {String|Object} module:engine/view/viewelementdefinition~ViewElementDefinition
36+
*
37+
* @property {String} name View element name.
38+
* @property {String|Array.<String>} [class] Class name or array of class names to match. Each name can be
39+
* provided in a form of string.
40+
* @property {Object} [style] Object with key-value pairs representing styles to match. Each object key
41+
* represents style name. Value under that key must be a string.
42+
* @property {Object} [attribute] Object with key-value pairs representing attributes to match. Each object key
43+
* represents attribute name. Value under that key must be a string.
44+
*/

0 commit comments

Comments
 (0)
This repository has been archived.