1 | /**
|
2 | * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
3 | * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4 | */
|
5 | /**
|
6 | * @module link/utils/automaticdecorators
|
7 | */
|
8 | import { toMap } from 'ckeditor5/src/utils';
|
9 | /**
|
10 | * Helper class that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition} and provides
|
11 | * the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatchers} for them.
|
12 | */
|
13 | export default class AutomaticDecorators {
|
14 | constructor() {
|
15 | /**
|
16 | * Stores the definition of {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}.
|
17 | * This data is used as a source for a downcast dispatcher to create a proper conversion to output data.
|
18 | */
|
19 | this._definitions = new Set();
|
20 | }
|
21 | /**
|
22 | * Gives information about the number of decorators stored in the {@link module:link/utils/automaticdecorators~AutomaticDecorators}
|
23 | * instance.
|
24 | */
|
25 | get length() {
|
26 | return this._definitions.size;
|
27 | }
|
28 | /**
|
29 | * Adds automatic decorator objects or an array with them to be used during downcasting.
|
30 | *
|
31 | * @param item A configuration object of automatic rules for decorating links. It might also be an array of such objects.
|
32 | */
|
33 | add(item) {
|
34 | if (Array.isArray(item)) {
|
35 | item.forEach(item => this._definitions.add(item));
|
36 | }
|
37 | else {
|
38 | this._definitions.add(item);
|
39 | }
|
40 | }
|
41 | /**
|
42 | * Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method.
|
43 | *
|
44 | * @returns A dispatcher function used as conversion helper in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
|
45 | */
|
46 | getDispatcher() {
|
47 | return dispatcher => {
|
48 | dispatcher.on('attribute:linkHref', (evt, data, conversionApi) => {
|
49 | // There is only test as this behavior decorates links and
|
50 | // it is run before dispatcher which actually consumes this node.
|
51 | // This allows on writing own dispatcher with highest priority,
|
52 | // which blocks both native converter and this additional decoration.
|
53 | if (!conversionApi.consumable.test(data.item, 'attribute:linkHref')) {
|
54 | return;
|
55 | }
|
56 | // Automatic decorators for block links are handled e.g. in LinkImageEditing.
|
57 | if (!(data.item.is('selection') || conversionApi.schema.isInline(data.item))) {
|
58 | return;
|
59 | }
|
60 | const viewWriter = conversionApi.writer;
|
61 | const viewSelection = viewWriter.document.selection;
|
62 | for (const item of this._definitions) {
|
63 | const viewElement = viewWriter.createAttributeElement('a', item.attributes, {
|
64 | priority: 5
|
65 | });
|
66 | if (item.classes) {
|
67 | viewWriter.addClass(item.classes, viewElement);
|
68 | }
|
69 | for (const key in item.styles) {
|
70 | viewWriter.setStyle(key, item.styles[key], viewElement);
|
71 | }
|
72 | viewWriter.setCustomProperty('link', true, viewElement);
|
73 | if (item.callback(data.attributeNewValue)) {
|
74 | if (data.item.is('selection')) {
|
75 | viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
|
76 | }
|
77 | else {
|
78 | viewWriter.wrap(conversionApi.mapper.toViewRange(data.range), viewElement);
|
79 | }
|
80 | }
|
81 | else {
|
82 | viewWriter.unwrap(conversionApi.mapper.toViewRange(data.range), viewElement);
|
83 | }
|
84 | }
|
85 | }, { priority: 'high' });
|
86 | };
|
87 | }
|
88 | /**
|
89 | * Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method
|
90 | * when linking images.
|
91 | *
|
92 | * @returns A dispatcher function used as conversion helper in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
|
93 | */
|
94 | getDispatcherForLinkedImage() {
|
95 | return dispatcher => {
|
96 | dispatcher.on('attribute:linkHref:imageBlock', (evt, data, { writer, mapper }) => {
|
97 | const viewFigure = mapper.toViewElement(data.item);
|
98 | const linkInImage = Array.from(viewFigure.getChildren())
|
99 | .find((child) => child.is('element', 'a'));
|
100 | for (const item of this._definitions) {
|
101 | const attributes = toMap(item.attributes);
|
102 | if (item.callback(data.attributeNewValue)) {
|
103 | for (const [key, val] of attributes) {
|
104 | // Left for backward compatibility. Since v30 decorator should
|
105 | // accept `classes` and `styles` separately from `attributes`.
|
106 | if (key === 'class') {
|
107 | writer.addClass(val, linkInImage);
|
108 | }
|
109 | else {
|
110 | writer.setAttribute(key, val, linkInImage);
|
111 | }
|
112 | }
|
113 | if (item.classes) {
|
114 | writer.addClass(item.classes, linkInImage);
|
115 | }
|
116 | for (const key in item.styles) {
|
117 | writer.setStyle(key, item.styles[key], linkInImage);
|
118 | }
|
119 | }
|
120 | else {
|
121 | for (const [key, val] of attributes) {
|
122 | if (key === 'class') {
|
123 | writer.removeClass(val, linkInImage);
|
124 | }
|
125 | else {
|
126 | writer.removeAttribute(key, linkInImage);
|
127 | }
|
128 | }
|
129 | if (item.classes) {
|
130 | writer.removeClass(item.classes, linkInImage);
|
131 | }
|
132 | for (const key in item.styles) {
|
133 | writer.removeStyle(key, linkInImage);
|
134 | }
|
135 | }
|
136 | }
|
137 | });
|
138 | };
|
139 | }
|
140 | }
|