@ckeditor/ckeditor5-link 47.5.0 → 47.6.0-alpha.0
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/build/link.js +1 -1
- package/dist/index.js +346 -122
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/index.d.ts +1 -1
- package/src/linkcommand.js +9 -2
- package/src/linkconfig.d.ts +8 -0
- package/src/linkediting.d.ts +4 -0
- package/src/linkediting.js +114 -17
- package/src/linkimageediting.js +42 -32
- package/src/linkui.js +4 -2
- package/src/utils/automaticdecorators.d.ts +17 -1
- package/src/utils/automaticdecorators.js +109 -72
- package/src/utils/conflictingdecorators.d.ts +43 -0
- package/src/utils/conflictingdecorators.js +80 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @module link/utils/automaticdecorators
|
|
7
7
|
*/
|
|
8
|
-
import { toMap } from 'ckeditor5/src/utils.js';
|
|
8
|
+
import { toMap, priorities } from 'ckeditor5/src/utils.js';
|
|
9
9
|
/**
|
|
10
10
|
* Helper class that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition} and provides
|
|
11
11
|
* the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatchers} for them.
|
|
@@ -16,6 +16,11 @@ export class AutomaticLinkDecorators {
|
|
|
16
16
|
* This data is used as a source for a downcast dispatcher to create a proper conversion to output data.
|
|
17
17
|
*/
|
|
18
18
|
_definitions = new Set();
|
|
19
|
+
/**
|
|
20
|
+
* A callback that checks if a decorator can be applied to a given element.
|
|
21
|
+
* Returns `true` if there is a conflict preventing the decorator from being applied.
|
|
22
|
+
*/
|
|
23
|
+
_conflictChecker;
|
|
19
24
|
/**
|
|
20
25
|
* Gives information about the number of decorators stored in the {@link module:link/utils/automaticdecorators~AutomaticLinkDecorators}
|
|
21
26
|
* instance.
|
|
@@ -23,6 +28,14 @@ export class AutomaticLinkDecorators {
|
|
|
23
28
|
get length() {
|
|
24
29
|
return this._definitions.size;
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Sets a callback that checks if a decorator can be applied to a given element.
|
|
33
|
+
*
|
|
34
|
+
* @param checker A function that returns `true` if there is a conflict preventing the decorator from being applied.
|
|
35
|
+
*/
|
|
36
|
+
setConflictChecker(checker) {
|
|
37
|
+
this._conflictChecker = checker;
|
|
38
|
+
}
|
|
26
39
|
/**
|
|
27
40
|
* Adds automatic decorator objects or an array with them to be used during downcasting.
|
|
28
41
|
*
|
|
@@ -43,44 +56,57 @@ export class AutomaticLinkDecorators {
|
|
|
43
56
|
*/
|
|
44
57
|
getDispatcher() {
|
|
45
58
|
return dispatcher => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return;
|
|
59
|
+
const elementCreator = (item, viewWriter) => {
|
|
60
|
+
const viewElement = viewWriter.createAttributeElement('a', item.attributes, {
|
|
61
|
+
priority: 5
|
|
62
|
+
});
|
|
63
|
+
if (item.classes) {
|
|
64
|
+
viewWriter.addClass(item.classes, viewElement);
|
|
53
65
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
66
|
+
for (const key in item.styles) {
|
|
67
|
+
viewWriter.setStyle(key, item.styles[key], viewElement);
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
viewWriter.setCustomProperty('link', true, viewElement);
|
|
70
|
+
return viewElement;
|
|
71
|
+
};
|
|
72
|
+
const createConverter = (isApplyingConverter) => {
|
|
73
|
+
return (evt, data, conversionApi) => {
|
|
74
|
+
if (!data.attributeKey.startsWith('link')) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// There is only test as this behavior decorates links and
|
|
78
|
+
// it is run before dispatcher which actually consumes this node.
|
|
79
|
+
// This allows on writing own dispatcher with highest priority,
|
|
80
|
+
// which blocks both native converter and this additional decoration.
|
|
81
|
+
if (data.attributeKey == 'linkHref' && !conversionApi.consumable.test(data.item, 'attribute:linkHref')) {
|
|
82
|
+
return;
|
|
66
83
|
}
|
|
67
|
-
for
|
|
68
|
-
|
|
84
|
+
// Automatic decorators for block links are handled e.g. in LinkImageEditing.
|
|
85
|
+
if (!data.item.is('selection') && !conversionApi.schema.isInline(data.item)) {
|
|
86
|
+
return;
|
|
69
87
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (data.item.
|
|
73
|
-
|
|
88
|
+
for (const decorator of this._definitions) {
|
|
89
|
+
// Check if automatic decorator is matched and does not conflict with any other active manual decorator.
|
|
90
|
+
if (decorator.callback(data.item.getAttribute('linkHref')) &&
|
|
91
|
+
!this._conflictChecker?.(decorator, data.item) &&
|
|
92
|
+
isApplyingConverter) {
|
|
93
|
+
if (data.item.is('selection')) {
|
|
94
|
+
conversionApi.writer.wrap(conversionApi.writer.document.selection.getFirstRange(), elementCreator(decorator, conversionApi.writer));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
conversionApi.writer.wrap(conversionApi.mapper.toViewRange(data.range), elementCreator(decorator, conversionApi.writer));
|
|
98
|
+
}
|
|
74
99
|
}
|
|
75
100
|
else {
|
|
76
|
-
|
|
101
|
+
conversionApi.writer.unwrap(conversionApi.mapper.toViewRange(data.range), elementCreator(decorator, conversionApi.writer));
|
|
77
102
|
}
|
|
78
103
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
dispatcher.on('attribute', createConverter(false), { priority: priorities.high - 1 });
|
|
107
|
+
// Apply decorators after all automatic and manual decorators are removed so removing one decorator
|
|
108
|
+
// won't strip part of the other decorator's attributes, classes or styles.
|
|
109
|
+
dispatcher.on('attribute', createConverter(true), { priority: priorities.high - 2 });
|
|
84
110
|
};
|
|
85
111
|
}
|
|
86
112
|
/**
|
|
@@ -91,54 +117,65 @@ export class AutomaticLinkDecorators {
|
|
|
91
117
|
*/
|
|
92
118
|
getDispatcherForLinkedImage() {
|
|
93
119
|
return dispatcher => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
const createConverter = (isApplyingConverter) => {
|
|
121
|
+
return (evt, data, { writer, mapper }) => {
|
|
122
|
+
if (!data.item.is('element', 'imageBlock') || !data.attributeKey.startsWith('link')) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const viewFigure = mapper.toViewElement(data.item);
|
|
126
|
+
const linkInImage = Array.from(viewFigure.getChildren())
|
|
127
|
+
.find((child) => child.is('element', 'a'));
|
|
128
|
+
// It's not guaranteed that the anchor is present in the image block during execution of this dispatcher.
|
|
129
|
+
// It might have been removed during the execution of unlink command that runs the image link downcast dispatcher
|
|
130
|
+
// that is executed before this one and removes the anchor from the image block.
|
|
131
|
+
if (!linkInImage) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const decorator of this._definitions) {
|
|
135
|
+
const attributes = toMap(decorator.attributes);
|
|
136
|
+
if (decorator.callback(data.item.getAttribute('linkHref')) &&
|
|
137
|
+
!this._conflictChecker?.(decorator, data.item) &&
|
|
138
|
+
isApplyingConverter) {
|
|
139
|
+
for (const [key, val] of attributes) {
|
|
140
|
+
// Left for backward compatibility. Since v30 decorator should
|
|
141
|
+
// accept `classes` and `styles` separately from `attributes`.
|
|
142
|
+
if (key === 'class') {
|
|
143
|
+
writer.addClass(val, linkInImage);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
writer.setAttribute(key, val, false, linkInImage);
|
|
147
|
+
}
|
|
112
148
|
}
|
|
113
|
-
|
|
114
|
-
writer.
|
|
149
|
+
if (decorator.classes) {
|
|
150
|
+
writer.addClass(decorator.classes, linkInImage);
|
|
151
|
+
}
|
|
152
|
+
for (const key in decorator.styles) {
|
|
153
|
+
writer.setStyle(key, decorator.styles[key], linkInImage);
|
|
115
154
|
}
|
|
116
155
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
for (const [key, val] of attributes) {
|
|
126
|
-
if (key === 'class') {
|
|
127
|
-
writer.removeClass(val, linkInImage);
|
|
156
|
+
else {
|
|
157
|
+
for (const [key, val] of attributes) {
|
|
158
|
+
if (key === 'class') {
|
|
159
|
+
writer.removeClass(val, linkInImage);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
writer.removeAttribute(key, val, linkInImage);
|
|
163
|
+
}
|
|
128
164
|
}
|
|
129
|
-
|
|
130
|
-
writer.
|
|
165
|
+
if (decorator.classes) {
|
|
166
|
+
writer.removeClass(decorator.classes, linkInImage);
|
|
167
|
+
}
|
|
168
|
+
for (const key in decorator.styles) {
|
|
169
|
+
writer.removeStyle(key, linkInImage);
|
|
131
170
|
}
|
|
132
|
-
}
|
|
133
|
-
if (item.classes) {
|
|
134
|
-
writer.removeClass(item.classes, linkInImage);
|
|
135
|
-
}
|
|
136
|
-
for (const key in item.styles) {
|
|
137
|
-
writer.removeStyle(key, linkInImage);
|
|
138
171
|
}
|
|
139
172
|
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
dispatcher.on('attribute', createConverter(false), { priority: priorities.high - 1 });
|
|
176
|
+
// Apply decorators after all automatic and manual decorators are removed so removing one decorator
|
|
177
|
+
// won't strip part of the other decorator's attributes, classes or styles.
|
|
178
|
+
dispatcher.on('attribute', createConverter(true), { priority: priorities.high - 2 });
|
|
142
179
|
};
|
|
143
180
|
}
|
|
144
181
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, 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 link/utils/conflictingdecorators
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Checks if two decorators conflict with each other.
|
|
10
|
+
*
|
|
11
|
+
* Decorators conflict when they share the same HTML attribute names (excluding mergeable attributes)
|
|
12
|
+
* or style properties.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
* @param a The first decorator.
|
|
16
|
+
* @param b The second decorator.
|
|
17
|
+
*/
|
|
18
|
+
export declare function areDecoratorsConflicting(a: DecoratorLike, b: DecoratorLike): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves conflicting manual decorators by automatically disabling decorators that share
|
|
21
|
+
* the same HTML attributes with newly enabled decorators.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
* @param options Configuration object.
|
|
25
|
+
* @param options.decoratorStates Initial decorator states.
|
|
26
|
+
* @param options.allDecorators Collection of all manual decorators.
|
|
27
|
+
* @returns Updated decorator states with conflicts resolved.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveConflictingDecorators({ decoratorStates, allDecorators }: {
|
|
30
|
+
decoratorStates: Record<string, boolean>;
|
|
31
|
+
allDecorators: Array<DecoratorLike & {
|
|
32
|
+
value?: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
}): Record<string, boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Decorator-like object representing attributes and styles.
|
|
37
|
+
*/
|
|
38
|
+
type DecoratorLike = {
|
|
39
|
+
id: string;
|
|
40
|
+
attributes?: Record<string, string>;
|
|
41
|
+
styles?: Record<string, string>;
|
|
42
|
+
};
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, 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 link/utils/conflictingdecorators
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Checks if two decorators conflict with each other.
|
|
10
|
+
*
|
|
11
|
+
* Decorators conflict when they share the same HTML attribute names (excluding mergeable attributes)
|
|
12
|
+
* or style properties.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
* @param a The first decorator.
|
|
16
|
+
* @param b The second decorator.
|
|
17
|
+
*/
|
|
18
|
+
export function areDecoratorsConflicting(a, b) {
|
|
19
|
+
if (a.attributes && b.attributes) {
|
|
20
|
+
const hasConflict = Object.keys(a.attributes).some(key => !isMergeableAttribute(key) && key in b.attributes);
|
|
21
|
+
if (hasConflict) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Check for conflicting style properties (same CSS property names).
|
|
26
|
+
if (a.styles && b.styles) {
|
|
27
|
+
const hasConflict = Object.keys(a.styles).some(key => key in b.styles);
|
|
28
|
+
if (hasConflict) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Classes don't conflict with each other - they can be merged.
|
|
33
|
+
return false;
|
|
34
|
+
function isMergeableAttribute(key) {
|
|
35
|
+
return key === 'class' || key === 'style' || key === 'rel';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves conflicting manual decorators by automatically disabling decorators that share
|
|
40
|
+
* the same HTML attributes with newly enabled decorators.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
* @param options Configuration object.
|
|
44
|
+
* @param options.decoratorStates Initial decorator states.
|
|
45
|
+
* @param options.allDecorators Collection of all manual decorators.
|
|
46
|
+
* @returns Updated decorator states with conflicts resolved.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveConflictingDecorators({ decoratorStates, allDecorators }) {
|
|
49
|
+
const resolved = { ...decoratorStates };
|
|
50
|
+
for (const name in decoratorStates) {
|
|
51
|
+
if (decoratorStates[name] && isNewlyAddedDecorator(name)) {
|
|
52
|
+
const conflicts = getConflictingManualDecorators(name, allDecorators);
|
|
53
|
+
for (const conflict of conflicts) {
|
|
54
|
+
resolved[conflict] = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isNewlyAddedDecorator(name) {
|
|
59
|
+
return allDecorators.some(item => item.id === name && !item.value);
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns array of decorator names that conflict with the given decorator.
|
|
65
|
+
* Decorators conflict when they share the same HTML attribute names or style properties.
|
|
66
|
+
*
|
|
67
|
+
* @param decoratorId The id/name of the manual decorator to check for conflicts.
|
|
68
|
+
* @param manualDecorators Collection of all manual decorators.
|
|
69
|
+
* @returns Array of conflicting decorator names.
|
|
70
|
+
*/
|
|
71
|
+
function getConflictingManualDecorators(decoratorId, manualDecorators) {
|
|
72
|
+
const decorator = manualDecorators.find(item => item.id === decoratorId);
|
|
73
|
+
/* istanbul ignore next -- @preserve */
|
|
74
|
+
if (!decorator) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
return manualDecorators
|
|
78
|
+
.filter(otherDecorator => otherDecorator.id !== decoratorId && areDecoratorsConflicting(decorator, otherDecorator))
|
|
79
|
+
.map(item => item.id);
|
|
80
|
+
}
|