@ckeditor/ckeditor5-html-support 44.2.1 → 44.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-html-support",
3
- "version": "44.2.1",
3
+ "version": "44.3.0-alpha.1",
4
4
  "description": "HTML Support feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -17,16 +17,16 @@
17
17
  "type": "module",
18
18
  "main": "src/index.js",
19
19
  "dependencies": {
20
- "@ckeditor/ckeditor5-core": "44.2.1",
21
- "@ckeditor/ckeditor5-engine": "44.2.1",
22
- "@ckeditor/ckeditor5-enter": "44.2.1",
23
- "@ckeditor/ckeditor5-heading": "44.2.1",
24
- "@ckeditor/ckeditor5-image": "44.2.1",
25
- "@ckeditor/ckeditor5-list": "44.2.1",
26
- "@ckeditor/ckeditor5-table": "44.2.1",
27
- "@ckeditor/ckeditor5-utils": "44.2.1",
28
- "@ckeditor/ckeditor5-widget": "44.2.1",
29
- "ckeditor5": "44.2.1",
20
+ "@ckeditor/ckeditor5-core": "44.3.0-alpha.1",
21
+ "@ckeditor/ckeditor5-engine": "44.3.0-alpha.1",
22
+ "@ckeditor/ckeditor5-enter": "44.3.0-alpha.1",
23
+ "@ckeditor/ckeditor5-heading": "44.3.0-alpha.1",
24
+ "@ckeditor/ckeditor5-image": "44.3.0-alpha.1",
25
+ "@ckeditor/ckeditor5-list": "44.3.0-alpha.1",
26
+ "@ckeditor/ckeditor5-table": "44.3.0-alpha.1",
27
+ "@ckeditor/ckeditor5-utils": "44.3.0-alpha.1",
28
+ "@ckeditor/ckeditor5-widget": "44.3.0-alpha.1",
29
+ "ckeditor5": "44.3.0-alpha.1",
30
30
  "lodash-es": "4.17.21"
31
31
  },
32
32
  "author": "CKSource (http://cksource.com/)",
@@ -2,7 +2,7 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import type { GeneralHtmlSupport, DataFilter, DataSchema, GeneralHtmlSupportConfig, CodeBlockElementSupport, CustomElementSupport, ListElementSupport, DualContentModelElementSupport, HeadingElementSupport, ImageElementSupport, MediaEmbedElementSupport, ScriptElementSupport, StyleElementSupport, TableElementSupport, HtmlComment, FullPage } from './index.js';
5
+ import type { GeneralHtmlSupport, DataFilter, DataSchema, GeneralHtmlSupportConfig, CodeBlockElementSupport, CustomElementSupport, ListElementSupport, DualContentModelElementSupport, HeadingElementSupport, ImageElementSupport, MediaEmbedElementSupport, ScriptElementSupport, StyleElementSupport, TableElementSupport, HorizontalLineElementSupport, HtmlComment, FullPage, EmptyBlock } from './index.js';
6
6
  declare module '@ckeditor/ckeditor5-core' {
7
7
  interface EditorConfig {
8
8
  /**
@@ -27,7 +27,9 @@ declare module '@ckeditor/ckeditor5-core' {
27
27
  [ScriptElementSupport.pluginName]: ScriptElementSupport;
28
28
  [StyleElementSupport.pluginName]: StyleElementSupport;
29
29
  [TableElementSupport.pluginName]: TableElementSupport;
30
+ [HorizontalLineElementSupport.pluginName]: HorizontalLineElementSupport;
30
31
  [HtmlComment.pluginName]: HtmlComment;
31
32
  [FullPage.pluginName]: FullPage;
33
+ [EmptyBlock.pluginName]: EmptyBlock;
32
34
  }
33
35
  }
package/src/datafilter.js CHANGED
@@ -528,6 +528,11 @@ export default class DataFilter extends Plugin {
528
528
  const conversion = editor.conversion;
529
529
  const { view: viewName, model: modelName } = definition;
530
530
  if (!schema.isRegistered(definition.model)) {
531
+ // Do not register converters and empty schema for editor existing feature
532
+ // as empty schema won't allow element anywhere in the model.
533
+ if (!definition.modelSchema) {
534
+ return;
535
+ }
531
536
  schema.register(definition.model, definition.modelSchema);
532
537
  if (!viewName) {
533
538
  return;
@@ -542,7 +547,9 @@ export default class DataFilter extends Plugin {
542
547
  });
543
548
  conversion.for('downcast').elementToElement({
544
549
  model: modelName,
545
- view: viewName
550
+ view: (modelElement, { writer }) => definition.isEmpty ?
551
+ writer.createEmptyElement(viewName) :
552
+ writer.createContainerElement(viewName)
546
553
  });
547
554
  }
548
555
  if (!viewName) {
@@ -623,35 +630,39 @@ function matchAndConsumeAttributes(viewElement, matcher, consumable) {
623
630
  const matches = matcher.matchAll(viewElement) || [];
624
631
  const stylesProcessor = viewElement.document.stylesProcessor;
625
632
  return matches.reduce((result, { match }) => {
626
- // Verify and consume styles.
627
- for (const style of match.styles || []) {
628
- // Check longer forms of the same style as those could be matched
629
- // but not present in the element directly.
630
- // Consider only longhand (or longer than current notation) so that
631
- // we do not include all sides of the box if only one side is allowed.
632
- const sortedRelatedStyles = stylesProcessor.getRelatedStyles(style)
633
- .filter(relatedStyle => relatedStyle.split('-').length > style.split('-').length)
634
- .sort((a, b) => b.split('-').length - a.split('-').length);
635
- for (const relatedStyle of sortedRelatedStyles) {
636
- if (consumable.consume(viewElement, { styles: [relatedStyle] })) {
637
- result.styles.push(relatedStyle);
633
+ for (const [key, token] of match.attributes || []) {
634
+ // Verify and consume styles.
635
+ if (key == 'style') {
636
+ const style = token;
637
+ // Check longer forms of the same style as those could be matched
638
+ // but not present in the element directly.
639
+ // Consider only longhand (or longer than current notation) so that
640
+ // we do not include all sides of the box if only one side is allowed.
641
+ const sortedRelatedStyles = stylesProcessor.getRelatedStyles(style)
642
+ .filter(relatedStyle => relatedStyle.split('-').length > style.split('-').length)
643
+ .sort((a, b) => b.split('-').length - a.split('-').length);
644
+ for (const relatedStyle of sortedRelatedStyles) {
645
+ if (consumable.consume(viewElement, { styles: [relatedStyle] })) {
646
+ result.styles.push(relatedStyle);
647
+ }
648
+ }
649
+ // Verify and consume style as specified in the matcher.
650
+ if (consumable.consume(viewElement, { styles: [style] })) {
651
+ result.styles.push(style);
638
652
  }
639
653
  }
640
- // Verify and consume style as specified in the matcher.
641
- if (consumable.consume(viewElement, { styles: [style] })) {
642
- result.styles.push(style);
643
- }
644
- }
645
- // Verify and consume class names.
646
- for (const className of match.classes || []) {
647
- if (consumable.consume(viewElement, { classes: [className] })) {
648
- result.classes.push(className);
654
+ // Verify and consume class names.
655
+ else if (key == 'class') {
656
+ const className = token;
657
+ if (consumable.consume(viewElement, { classes: [className] })) {
658
+ result.classes.push(className);
659
+ }
649
660
  }
650
- }
651
- // Verify and consume other attributes.
652
- for (const attributeName of match.attributes || []) {
653
- if (consumable.consume(viewElement, { attributes: [attributeName] })) {
654
- result.attributes.push(attributeName);
661
+ else {
662
+ // Verify and consume other attributes.
663
+ if (consumable.consume(viewElement, { attributes: [key] })) {
664
+ result.attributes.push(key);
665
+ }
655
666
  }
656
667
  }
657
668
  return result;
@@ -141,6 +141,10 @@ export interface DataSchemaDefinition {
141
141
  * Indicates that the definition describes inline element.
142
142
  */
143
143
  isInline?: boolean;
144
+ /**
145
+ * Indicates that the definition describes an empty HTML element like `<hr>`.
146
+ */
147
+ isEmpty?: boolean;
144
148
  }
145
149
  /**
146
150
  * A definition of {@link module:html-support/dataschema~DataSchema data schema} for block elements.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ /**
7
+ * This is experimental plugin that allows for preserving empty block elements
8
+ * in the editor content instead of automatically filling them with block fillers (`&nbsp;`).
9
+ *
10
+ * This is useful when you want to:
11
+ *
12
+ * * Preserve empty block elements exactly as they were in the source HTML.
13
+ * * Allow for styling empty blocks with CSS (block fillers can interfere with height/margin).
14
+ * * Maintain compatibility with external systems that expect empty blocks to remain empty.
15
+ *
16
+ * Known limitations:
17
+ *
18
+ * * Empty blocks may not work correctly with revision history features.
19
+ * * Keyboard navigation through the document might behave unexpectedly, especially when
20
+ * navigating through structures like lists and tables.
21
+ *
22
+ * For example, this allows for HTML like:
23
+ *
24
+ * ```html
25
+ * <p></p>
26
+ * <p class="spacer"></p>
27
+ * <td></td>
28
+ * ```
29
+ * to remain empty instead of being converted to:
30
+ *
31
+ * ```html
32
+ * <p>&nbsp;</p>
33
+ * <p class="spacer">&nbsp;</p>
34
+ * <td>&nbsp;</td>
35
+ * ```
36
+ *
37
+ * **Warning**: This is an experimental plugin. It may have bugs and breaking changes may be introduced without prior notice.
38
+ */
39
+ export default class EmptyBlock extends Plugin {
40
+ /**
41
+ * @inheritDoc
42
+ */
43
+ static get pluginName(): "EmptyBlock";
44
+ /**
45
+ * @inheritDoc
46
+ */
47
+ static get isOfficialPlugin(): true;
48
+ /**
49
+ * @inheritDoc
50
+ */
51
+ afterInit(): void;
52
+ /**
53
+ * Handle clipboard paste events:
54
+ *
55
+ * * It does not affect *copying* content from the editor, only *pasting*.
56
+ * * When content is pasted from another editor instance with `<p></p>`,
57
+ * the `&nbsp;` filler is added, so the getData result is `<p>&nbsp;</p>`.
58
+ * * When content is pasted from the same editor instance with `<p></p>`,
59
+ * the `&nbsp;` filler is not added, so the getData result is `<p></p>`.
60
+ */
61
+ private _registerClipboardPastingHandler;
62
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ const EMPTY_BLOCK_MODEL_ATTRIBUTE = 'htmlEmptyBlock';
7
+ /**
8
+ * This is experimental plugin that allows for preserving empty block elements
9
+ * in the editor content instead of automatically filling them with block fillers (`&nbsp;`).
10
+ *
11
+ * This is useful when you want to:
12
+ *
13
+ * * Preserve empty block elements exactly as they were in the source HTML.
14
+ * * Allow for styling empty blocks with CSS (block fillers can interfere with height/margin).
15
+ * * Maintain compatibility with external systems that expect empty blocks to remain empty.
16
+ *
17
+ * Known limitations:
18
+ *
19
+ * * Empty blocks may not work correctly with revision history features.
20
+ * * Keyboard navigation through the document might behave unexpectedly, especially when
21
+ * navigating through structures like lists and tables.
22
+ *
23
+ * For example, this allows for HTML like:
24
+ *
25
+ * ```html
26
+ * <p></p>
27
+ * <p class="spacer"></p>
28
+ * <td></td>
29
+ * ```
30
+ * to remain empty instead of being converted to:
31
+ *
32
+ * ```html
33
+ * <p>&nbsp;</p>
34
+ * <p class="spacer">&nbsp;</p>
35
+ * <td>&nbsp;</td>
36
+ * ```
37
+ *
38
+ * **Warning**: This is an experimental plugin. It may have bugs and breaking changes may be introduced without prior notice.
39
+ */
40
+ export default class EmptyBlock extends Plugin {
41
+ /**
42
+ * @inheritDoc
43
+ */
44
+ static get pluginName() {
45
+ return 'EmptyBlock';
46
+ }
47
+ /**
48
+ * @inheritDoc
49
+ */
50
+ static get isOfficialPlugin() {
51
+ return true;
52
+ }
53
+ /**
54
+ * @inheritDoc
55
+ */
56
+ afterInit() {
57
+ const { model, conversion, plugins, config } = this.editor;
58
+ const schema = model.schema;
59
+ const preserveEmptyBlocksInEditingView = config.get('htmlSupport.preserveEmptyBlocksInEditingView');
60
+ schema.extend('$block', { allowAttributes: [EMPTY_BLOCK_MODEL_ATTRIBUTE] });
61
+ schema.extend('$container', { allowAttributes: [EMPTY_BLOCK_MODEL_ATTRIBUTE] });
62
+ if (schema.isRegistered('tableCell')) {
63
+ schema.extend('tableCell', { allowAttributes: [EMPTY_BLOCK_MODEL_ATTRIBUTE] });
64
+ }
65
+ if (preserveEmptyBlocksInEditingView) {
66
+ conversion.for('downcast').add(createEmptyBlockDowncastConverter());
67
+ }
68
+ else {
69
+ conversion.for('dataDowncast').add(createEmptyBlockDowncastConverter());
70
+ }
71
+ conversion.for('upcast').add(createEmptyBlockUpcastConverter(schema));
72
+ if (plugins.has('ClipboardPipeline')) {
73
+ this._registerClipboardPastingHandler();
74
+ }
75
+ }
76
+ /**
77
+ * Handle clipboard paste events:
78
+ *
79
+ * * It does not affect *copying* content from the editor, only *pasting*.
80
+ * * When content is pasted from another editor instance with `<p></p>`,
81
+ * the `&nbsp;` filler is added, so the getData result is `<p>&nbsp;</p>`.
82
+ * * When content is pasted from the same editor instance with `<p></p>`,
83
+ * the `&nbsp;` filler is not added, so the getData result is `<p></p>`.
84
+ */
85
+ _registerClipboardPastingHandler() {
86
+ const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
87
+ this.listenTo(clipboardPipeline, 'contentInsertion', (evt, data) => {
88
+ if (data.sourceEditorId === this.editor.id) {
89
+ return;
90
+ }
91
+ this.editor.model.change(writer => {
92
+ for (const { item } of writer.createRangeIn(data.content)) {
93
+ if (item.is('element') && item.hasAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
94
+ writer.removeAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, item);
95
+ }
96
+ }
97
+ });
98
+ });
99
+ }
100
+ }
101
+ /**
102
+ * Creates a downcast converter for handling empty blocks.
103
+ * This converter prevents filler elements from being added to elements marked as empty blocks.
104
+ */
105
+ function createEmptyBlockDowncastConverter() {
106
+ return (dispatcher) => {
107
+ dispatcher.on(`attribute:${EMPTY_BLOCK_MODEL_ATTRIBUTE}`, (evt, data, conversionApi) => {
108
+ const { mapper, consumable } = conversionApi;
109
+ const { item } = data;
110
+ if (!consumable.consume(item, evt.name)) {
111
+ return;
112
+ }
113
+ const viewElement = mapper.toViewElement(item);
114
+ if (viewElement && data.attributeNewValue) {
115
+ viewElement.getFillerOffset = () => null;
116
+ }
117
+ });
118
+ };
119
+ }
120
+ /**
121
+ * Creates an upcast converter for handling empty blocks.
122
+ * The converter detects empty elements and marks them with the empty block attribute.
123
+ */
124
+ function createEmptyBlockUpcastConverter(schema) {
125
+ return (dispatcher) => {
126
+ dispatcher.on('element', (evt, data, conversionApi) => {
127
+ const { viewItem, modelRange } = data;
128
+ if (!viewItem.is('element') || !viewItem.isEmpty || viewItem.getCustomProperty('$hasBlockFiller')) {
129
+ return;
130
+ }
131
+ // Handle element itself.
132
+ const modelElement = modelRange && modelRange.start.nodeAfter;
133
+ if (!modelElement || !schema.checkAttribute(modelElement, EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
134
+ return;
135
+ }
136
+ conversionApi.writer.setAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, true, modelElement);
137
+ // Handle an auto-paragraphed bogus paragraph inside empty element.
138
+ if (modelElement.childCount != 1) {
139
+ return;
140
+ }
141
+ const firstModelChild = modelElement.getChild(0);
142
+ if (firstModelChild.is('element', 'paragraph') &&
143
+ schema.checkAttribute(firstModelChild, EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
144
+ conversionApi.writer.setAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, true, firstModelChild);
145
+ }
146
+ }, { priority: 'lowest' });
147
+ };
148
+ }
@@ -17,6 +17,7 @@ import ScriptElementSupport from './integrations/script.js';
17
17
  import TableElementSupport from './integrations/table.js';
18
18
  import StyleElementSupport from './integrations/style.js';
19
19
  import ListElementSupport from './integrations/list.js';
20
+ import HorizontalLineElementSupport from './integrations/horizontalline.js';
20
21
  import CustomElementSupport from './integrations/customelement.js';
21
22
  import type { Selectable } from 'ckeditor5/src/engine.js';
22
23
  /**
@@ -37,7 +38,7 @@ export default class GeneralHtmlSupport extends Plugin {
37
38
  /**
38
39
  * @inheritDoc
39
40
  */
40
- static get requires(): readonly [typeof DataFilter, typeof CodeBlockElementSupport, typeof DualContentModelElementSupport, typeof HeadingElementSupport, typeof ImageElementSupport, typeof MediaEmbedElementSupport, typeof ScriptElementSupport, typeof TableElementSupport, typeof StyleElementSupport, typeof ListElementSupport, typeof CustomElementSupport];
41
+ static get requires(): readonly [typeof DataFilter, typeof CodeBlockElementSupport, typeof DualContentModelElementSupport, typeof HeadingElementSupport, typeof ImageElementSupport, typeof MediaEmbedElementSupport, typeof ScriptElementSupport, typeof TableElementSupport, typeof StyleElementSupport, typeof ListElementSupport, typeof HorizontalLineElementSupport, typeof CustomElementSupport];
41
42
  /**
42
43
  * @inheritDoc
43
44
  */
@@ -17,6 +17,7 @@ import ScriptElementSupport from './integrations/script.js';
17
17
  import TableElementSupport from './integrations/table.js';
18
18
  import StyleElementSupport from './integrations/style.js';
19
19
  import ListElementSupport from './integrations/list.js';
20
+ import HorizontalLineElementSupport from './integrations/horizontalline.js';
20
21
  import CustomElementSupport from './integrations/customelement.js';
21
22
  import { getHtmlAttributeName, modifyGhsAttribute } from './utils.js';
22
23
  /**
@@ -53,6 +54,7 @@ export default class GeneralHtmlSupport extends Plugin {
53
54
  TableElementSupport,
54
55
  StyleElementSupport,
55
56
  ListElementSupport,
57
+ HorizontalLineElementSupport,
56
58
  CustomElementSupport
57
59
  ];
58
60
  }
@@ -74,4 +74,16 @@ export interface GeneralHtmlSupportConfig {
74
74
  * ```
75
75
  */
76
76
  allowEmpty?: Array<string>;
77
+ /**
78
+ * Whether a filler text (non-breaking space entity — `&nbsp;`) will be inserted into empty block elements in HTML output.
79
+ * This is used to render block elements properly with line-height.
80
+ *
81
+ * When set to `true`, empty blocks will be preserved in the editing view.
82
+ * When `false` (default), empty blocks are only preserved in the data output.
83
+ *
84
+ * The option is used by the {@link module:html-support/emptyblock~EmptyBlock} feature.
85
+ *
86
+ * @default false
87
+ */
88
+ preserveEmptyBlocksInEditingView?: boolean;
77
89
  }
package/src/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { default as DataSchema, type DataSchemaBlockElementDefinition } from './
11
11
  export { default as HtmlComment } from './htmlcomment.js';
12
12
  export { default as FullPage } from './fullpage.js';
13
13
  export { default as HtmlPageDataProcessor } from './htmlpagedataprocessor.js';
14
+ export { default as EmptyBlock } from './emptyblock.js';
14
15
  export type { GeneralHtmlSupportConfig } from './generalhtmlsupportconfig.js';
15
16
  export type { default as CodeBlockElementSupport } from './integrations/codeblock.js';
16
17
  export type { default as CustomElementSupport } from './integrations/customelement.js';
@@ -22,4 +23,5 @@ export type { default as MediaEmbedElementSupport } from './integrations/mediaem
22
23
  export type { default as ScriptElementSupport } from './integrations/script.js';
23
24
  export type { default as StyleElementSupport } from './integrations/style.js';
24
25
  export type { default as TableElementSupport } from './integrations/table.js';
26
+ export type { default as HorizontalLineElementSupport } from './integrations/horizontalline.js';
25
27
  import './augmentation.js';
package/src/index.js CHANGED
@@ -11,4 +11,5 @@ export { default as DataSchema } from './dataschema.js';
11
11
  export { default as HtmlComment } from './htmlcomment.js';
12
12
  export { default as FullPage } from './fullpage.js';
13
13
  export { default as HtmlPageDataProcessor } from './htmlpagedataprocessor.js';
14
+ export { default as EmptyBlock } from './emptyblock.js';
14
15
  import './augmentation.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ /**
6
+ * @module html-support/integrations/horizontalline
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core.js';
9
+ import DataFilter from '../datafilter.js';
10
+ /**
11
+ * Provides the General HTML Support integration with the {@link module:horizontal-line/horizontalline~HorizontalLine} feature.
12
+ */
13
+ export default class HorizontalLineElementSupport extends Plugin {
14
+ /**
15
+ * @inheritDoc
16
+ */
17
+ static get requires(): readonly [typeof DataFilter];
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ static get pluginName(): "HorizontalLineElementSupport";
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get isOfficialPlugin(): true;
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ init(): void;
30
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ /**
6
+ * @module html-support/integrations/horizontalline
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core.js';
9
+ import DataFilter from '../datafilter.js';
10
+ import { updateViewAttributes } from '../utils.js';
11
+ import { getDescendantElement } from './integrationutils.js';
12
+ import { viewToModelBlockAttributeConverter } from '../converters.js';
13
+ /**
14
+ * Provides the General HTML Support integration with the {@link module:horizontal-line/horizontalline~HorizontalLine} feature.
15
+ */
16
+ export default class HorizontalLineElementSupport extends Plugin {
17
+ /**
18
+ * @inheritDoc
19
+ */
20
+ static get requires() {
21
+ return [DataFilter];
22
+ }
23
+ /**
24
+ * @inheritDoc
25
+ */
26
+ static get pluginName() {
27
+ return 'HorizontalLineElementSupport';
28
+ }
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ static get isOfficialPlugin() {
33
+ return true;
34
+ }
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ init() {
39
+ const editor = this.editor;
40
+ if (!editor.plugins.has('HorizontalLineEditing')) {
41
+ return;
42
+ }
43
+ const schema = editor.model.schema;
44
+ const conversion = editor.conversion;
45
+ const dataFilter = editor.plugins.get(DataFilter);
46
+ dataFilter.on('register:hr', (evt, definition) => {
47
+ if (definition.model !== 'horizontalLine') {
48
+ return;
49
+ }
50
+ schema.extend('horizontalLine', {
51
+ allowAttributes: ['htmlHrAttributes']
52
+ });
53
+ conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, dataFilter));
54
+ conversion.for('downcast').add(modelToViewHorizontalLineAttributeConverter());
55
+ evt.stop();
56
+ });
57
+ }
58
+ }
59
+ /**
60
+ * A model-to-view conversion helper applying attributes from the
61
+ * {@link module:horizontal-line/horizontalline~HorizontalLine HorizontalLine} feature.
62
+ *
63
+ * @returns Returns a conversion callback.
64
+ */
65
+ function modelToViewHorizontalLineAttributeConverter() {
66
+ return (dispatcher) => {
67
+ dispatcher.on('attribute:htmlHrAttributes:horizontalLine', (evt, data, conversionApi) => {
68
+ if (!conversionApi.consumable.test(data.item, evt.name)) {
69
+ return;
70
+ }
71
+ const { attributeOldValue, attributeNewValue } = data;
72
+ const containerElement = conversionApi.mapper.toViewElement(data.item);
73
+ const viewElement = getDescendantElement(conversionApi.writer, containerElement, 'hr');
74
+ if (viewElement) {
75
+ updateViewAttributes(conversionApi.writer, attributeOldValue, attributeNewValue, viewElement);
76
+ conversionApi.consumable.consume(data.item, evt.name);
77
+ }
78
+ }, { priority: 'low' });
79
+ };
80
+ }
@@ -111,6 +111,10 @@ export default {
111
111
  model: 'imageInline',
112
112
  view: 'img'
113
113
  },
114
+ {
115
+ model: 'horizontalLine',
116
+ view: 'hr'
117
+ },
114
118
  // Compatibility features.
115
119
  {
116
120
  model: 'htmlP',
@@ -517,6 +521,14 @@ export default {
517
521
  inheritAllFrom: '$container',
518
522
  isBlock: false
519
523
  }
524
+ },
525
+ {
526
+ model: 'htmlHr',
527
+ view: 'hr',
528
+ isEmpty: true,
529
+ modelSchema: {
530
+ inheritAllFrom: '$blockObject'
531
+ }
520
532
  }
521
533
  ],
522
534
  inline: [