@ckeditor/ckeditor5-clipboard 0.0.0-nightly-next-20260117.0 → 0.0.0-nightly-20260118.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/lang/contexts.json +5 -0
- package/lang/translations/af.po +24 -0
- package/lang/translations/ar.po +24 -0
- package/lang/translations/ast.po +24 -0
- package/lang/translations/az.po +24 -0
- package/lang/translations/be.po +24 -0
- package/lang/translations/bg.po +24 -0
- package/lang/translations/bn.po +24 -0
- package/lang/translations/bs.po +24 -0
- package/lang/translations/ca.po +24 -0
- package/lang/translations/cs.po +24 -0
- package/lang/translations/da.po +24 -0
- package/lang/translations/de-ch.po +24 -0
- package/lang/translations/de.po +24 -0
- package/lang/translations/el.po +24 -0
- package/lang/translations/en-au.po +24 -0
- package/lang/translations/en-gb.po +24 -0
- package/lang/translations/en.po +24 -0
- package/lang/translations/eo.po +24 -0
- package/lang/translations/es-co.po +24 -0
- package/lang/translations/es.po +24 -0
- package/lang/translations/et.po +24 -0
- package/lang/translations/eu.po +24 -0
- package/lang/translations/fa.po +24 -0
- package/lang/translations/fi.po +24 -0
- package/lang/translations/fr.po +24 -0
- package/lang/translations/gl.po +24 -0
- package/lang/translations/gu.po +24 -0
- package/lang/translations/he.po +24 -0
- package/lang/translations/hi.po +24 -0
- package/lang/translations/hr.po +24 -0
- package/lang/translations/hu.po +24 -0
- package/lang/translations/hy.po +24 -0
- package/lang/translations/id.po +24 -0
- package/lang/translations/it.po +24 -0
- package/lang/translations/ja.po +24 -0
- package/lang/translations/jv.po +24 -0
- package/lang/translations/kk.po +24 -0
- package/lang/translations/km.po +24 -0
- package/lang/translations/kn.po +24 -0
- package/lang/translations/ko.po +24 -0
- package/lang/translations/ku.po +24 -0
- package/lang/translations/lt.po +24 -0
- package/lang/translations/lv.po +24 -0
- package/lang/translations/ms.po +24 -0
- package/lang/translations/nb.po +24 -0
- package/lang/translations/ne.po +24 -0
- package/lang/translations/nl.po +24 -0
- package/lang/translations/no.po +24 -0
- package/lang/translations/oc.po +24 -0
- package/lang/translations/pl.po +24 -0
- package/lang/translations/pt-br.po +24 -0
- package/lang/translations/pt.po +24 -0
- package/lang/translations/ro.po +24 -0
- package/lang/translations/ru.po +24 -0
- package/lang/translations/si.po +24 -0
- package/lang/translations/sk.po +24 -0
- package/lang/translations/sl.po +24 -0
- package/lang/translations/sq.po +24 -0
- package/lang/translations/sr-latn.po +24 -0
- package/lang/translations/sr.po +24 -0
- package/lang/translations/sv.po +24 -0
- package/lang/translations/th.po +24 -0
- package/lang/translations/ti.po +24 -0
- package/lang/translations/tk.po +24 -0
- package/lang/translations/tr.po +24 -0
- package/lang/translations/tt.po +24 -0
- package/lang/translations/ug.po +24 -0
- package/lang/translations/uk.po +24 -0
- package/lang/translations/ur.po +24 -0
- package/lang/translations/uz.po +24 -0
- package/lang/translations/vi.po +24 -0
- package/lang/translations/zh-cn.po +24 -0
- package/lang/translations/zh.po +24 -0
- package/package.json +43 -23
- package/src/augmentation.js +5 -0
- package/src/clipboard.js +66 -0
- package/src/clipboardmarkersutils.js +500 -0
- package/src/clipboardobserver.js +75 -0
- package/src/clipboardpipeline.js +287 -0
- package/src/dragdrop.js +619 -0
- package/src/dragdropblocktoolbar.js +124 -0
- package/src/dragdroptarget.js +390 -0
- package/src/index.js +23 -0
- package/src/lineview.js +46 -0
- package/src/pasteplaintext.js +102 -0
- package/src/utils/normalizeclipboarddata.js +28 -0
- package/src/utils/plaintexttohtml.js +39 -0
- package/src/utils/viewtoplaintext.js +96 -0
- package/theme/clipboard.css +39 -0
- /package/{dist → src}/augmentation.d.ts +0 -0
- /package/{dist → src}/clipboard.d.ts +0 -0
- /package/{dist → src}/clipboardmarkersutils.d.ts +0 -0
- /package/{dist → src}/clipboardobserver.d.ts +0 -0
- /package/{dist → src}/clipboardpipeline.d.ts +0 -0
- /package/{dist → src}/dragdrop.d.ts +0 -0
- /package/{dist → src}/dragdropblocktoolbar.d.ts +0 -0
- /package/{dist → src}/dragdroptarget.d.ts +0 -0
- /package/{dist → src}/index.d.ts +0 -0
- /package/{dist → src}/lineview.d.ts +0 -0
- /package/{dist → src}/pasteplaintext.d.ts +0 -0
- /package/{dist → src}/utils/normalizeclipboarddata.d.ts +0 -0
- /package/{dist → src}/utils/plaintexttohtml.d.ts +0 -0
- /package/{dist → src}/utils/viewtoplaintext.d.ts +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
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 clipboard/dragdropblocktoolbar
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import { env, global, DomEmitterMixin } from '@ckeditor/ckeditor5-utils';
|
|
10
|
+
import { ClipboardObserver } from './clipboardobserver.js';
|
|
11
|
+
/**
|
|
12
|
+
* Integration of a block Drag and Drop support with the block toolbar.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export class DragDropBlockToolbar extends Plugin {
|
|
17
|
+
/**
|
|
18
|
+
* Whether current dragging is started by block toolbar button dragging.
|
|
19
|
+
*/
|
|
20
|
+
_isBlockDragging = false;
|
|
21
|
+
/**
|
|
22
|
+
* DOM Emitter.
|
|
23
|
+
*/
|
|
24
|
+
_domEmitter = new (DomEmitterMixin())();
|
|
25
|
+
/**
|
|
26
|
+
* @inheritDoc
|
|
27
|
+
*/
|
|
28
|
+
static get pluginName() {
|
|
29
|
+
return 'DragDropBlockToolbar';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @inheritDoc
|
|
33
|
+
*/
|
|
34
|
+
static get isOfficialPlugin() {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @inheritDoc
|
|
39
|
+
*/
|
|
40
|
+
init() {
|
|
41
|
+
const editor = this.editor;
|
|
42
|
+
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => {
|
|
43
|
+
if (isReadOnly) {
|
|
44
|
+
this.forceDisabled('readOnlyMode');
|
|
45
|
+
this._isBlockDragging = false;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.clearForceDisabled('readOnlyMode');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
if (env.isAndroid) {
|
|
52
|
+
this.forceDisabled('noAndroidSupport');
|
|
53
|
+
}
|
|
54
|
+
if (editor.plugins.has('BlockToolbar')) {
|
|
55
|
+
const blockToolbar = editor.plugins.get('BlockToolbar');
|
|
56
|
+
const element = blockToolbar.buttonView.element;
|
|
57
|
+
this._domEmitter.listenTo(element, 'dragstart', (evt, data) => this._handleBlockDragStart(data));
|
|
58
|
+
this._domEmitter.listenTo(global.document, 'dragover', (evt, data) => this._handleBlockDragging(data));
|
|
59
|
+
this._domEmitter.listenTo(global.document, 'drop', (evt, data) => this._handleBlockDragging(data));
|
|
60
|
+
this._domEmitter.listenTo(global.document, 'dragend', () => this._handleBlockDragEnd(), { useCapture: true });
|
|
61
|
+
if (this.isEnabled) {
|
|
62
|
+
element.setAttribute('draggable', 'true');
|
|
63
|
+
}
|
|
64
|
+
this.on('change:isEnabled', (evt, name, isEnabled) => {
|
|
65
|
+
element.setAttribute('draggable', isEnabled ? 'true' : 'false');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @inheritDoc
|
|
71
|
+
*/
|
|
72
|
+
destroy() {
|
|
73
|
+
this._domEmitter.stopListening();
|
|
74
|
+
return super.destroy();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `dragstart` event handler.
|
|
78
|
+
*/
|
|
79
|
+
_handleBlockDragStart(domEvent) {
|
|
80
|
+
if (!this.isEnabled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const model = this.editor.model;
|
|
84
|
+
const selection = model.document.selection;
|
|
85
|
+
const view = this.editor.editing.view;
|
|
86
|
+
const blocks = Array.from(selection.getSelectedBlocks());
|
|
87
|
+
const draggedRange = model.createRange(model.createPositionBefore(blocks[0]), model.createPositionAfter(blocks[blocks.length - 1]));
|
|
88
|
+
model.change(writer => writer.setSelection(draggedRange));
|
|
89
|
+
this._isBlockDragging = true;
|
|
90
|
+
view.focus();
|
|
91
|
+
view.getObserver(ClipboardObserver).onDomEvent(domEvent);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The `dragover` and `drop` event handler.
|
|
95
|
+
*/
|
|
96
|
+
_handleBlockDragging(domEvent) {
|
|
97
|
+
if (!this.isEnabled || !this._isBlockDragging) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const clientX = domEvent.clientX + (this.editor.locale.contentLanguageDirection == 'ltr' ? 100 : -100);
|
|
101
|
+
const clientY = domEvent.clientY;
|
|
102
|
+
const target = document.elementFromPoint(clientX, clientY);
|
|
103
|
+
const view = this.editor.editing.view;
|
|
104
|
+
if (!target || !target.closest('.ck-editor__editable')) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
view.getObserver(ClipboardObserver).onDomEvent({
|
|
108
|
+
...domEvent,
|
|
109
|
+
type: domEvent.type,
|
|
110
|
+
dataTransfer: domEvent.dataTransfer,
|
|
111
|
+
target,
|
|
112
|
+
clientX,
|
|
113
|
+
clientY,
|
|
114
|
+
preventDefault: () => domEvent.preventDefault(),
|
|
115
|
+
stopPropagation: () => domEvent.stopPropagation()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The `dragend` event handler.
|
|
120
|
+
*/
|
|
121
|
+
_handleBlockDragEnd() {
|
|
122
|
+
this._isBlockDragging = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
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 clipboard/dragdroptarget
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import { global, Rect, DomEmitterMixin, delay, ResizeObserver } from '@ckeditor/ckeditor5-utils';
|
|
10
|
+
import { LineView } from './lineview.js';
|
|
11
|
+
import { throttle } from 'es-toolkit/compat';
|
|
12
|
+
/**
|
|
13
|
+
* Part of the Drag and Drop handling. Responsible for finding and displaying the drop target.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class DragDropTarget extends Plugin {
|
|
18
|
+
/**
|
|
19
|
+
* A delayed callback removing the drop marker.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
removeDropMarkerDelayed = delay(() => this.removeDropMarker(), 40);
|
|
24
|
+
/**
|
|
25
|
+
* A throttled callback updating the drop marker.
|
|
26
|
+
*/
|
|
27
|
+
_updateDropMarkerThrottled = throttle(targetRange => this._updateDropMarker(targetRange), 40);
|
|
28
|
+
/**
|
|
29
|
+
* A throttled callback reconverting the drop parker.
|
|
30
|
+
*/
|
|
31
|
+
_reconvertMarkerThrottled = throttle(() => {
|
|
32
|
+
if (this.editor.model.markers.has('drop-target')) {
|
|
33
|
+
this.editor.editing.reconvertMarker('drop-target');
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
/**
|
|
37
|
+
* The horizontal drop target line view.
|
|
38
|
+
*/
|
|
39
|
+
_dropTargetLineView = new LineView();
|
|
40
|
+
/**
|
|
41
|
+
* DOM Emitter.
|
|
42
|
+
*/
|
|
43
|
+
_domEmitter = new (DomEmitterMixin())();
|
|
44
|
+
/**
|
|
45
|
+
* Map of document scrollable elements.
|
|
46
|
+
*/
|
|
47
|
+
_scrollables = new Map();
|
|
48
|
+
/**
|
|
49
|
+
* @inheritDoc
|
|
50
|
+
*/
|
|
51
|
+
static get pluginName() {
|
|
52
|
+
return 'DragDropTarget';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @inheritDoc
|
|
56
|
+
*/
|
|
57
|
+
static get isOfficialPlugin() {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @inheritDoc
|
|
62
|
+
*/
|
|
63
|
+
init() {
|
|
64
|
+
this._setupDropMarker();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @inheritDoc
|
|
68
|
+
*/
|
|
69
|
+
destroy() {
|
|
70
|
+
this._domEmitter.stopListening();
|
|
71
|
+
for (const { resizeObserver } of this._scrollables.values()) {
|
|
72
|
+
resizeObserver.destroy();
|
|
73
|
+
}
|
|
74
|
+
this._updateDropMarkerThrottled.cancel();
|
|
75
|
+
this.removeDropMarkerDelayed.cancel();
|
|
76
|
+
this._reconvertMarkerThrottled.cancel();
|
|
77
|
+
return super.destroy();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Finds the drop target range and updates the drop marker.
|
|
81
|
+
*
|
|
82
|
+
* @return The updated drop target range or null if no valid range was found.
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
updateDropMarker(targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
86
|
+
this.removeDropMarkerDelayed.cancel();
|
|
87
|
+
const targetRange = findDropTargetRange(this.editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange);
|
|
88
|
+
/* istanbul ignore next -- @preserve */
|
|
89
|
+
if (!targetRange) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (draggedRange && draggedRange.containsRange(targetRange)) {
|
|
93
|
+
// Target range is inside the dragged range.
|
|
94
|
+
this.removeDropMarker();
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (targetRange && !this.editor.model.canEditAt(targetRange)) {
|
|
98
|
+
// Do not show drop marker if target place is not editable.
|
|
99
|
+
this.removeDropMarker();
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
this._updateDropMarkerThrottled(targetRange);
|
|
103
|
+
return targetRange;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Finds the final drop target range.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
getFinalDropRange(targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
111
|
+
const targetRange = findDropTargetRange(this.editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange);
|
|
112
|
+
// The dragging markers must be removed after searching for the target range because sometimes
|
|
113
|
+
// the target lands on the marker itself.
|
|
114
|
+
this.removeDropMarker();
|
|
115
|
+
return targetRange;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Removes the drop target marker.
|
|
119
|
+
*
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
removeDropMarker() {
|
|
123
|
+
const model = this.editor.model;
|
|
124
|
+
this.removeDropMarkerDelayed.cancel();
|
|
125
|
+
this._updateDropMarkerThrottled.cancel();
|
|
126
|
+
this._dropTargetLineView.isVisible = false;
|
|
127
|
+
if (model.markers.has('drop-target')) {
|
|
128
|
+
model.change(writer => {
|
|
129
|
+
writer.removeMarker('drop-target');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Creates downcast conversion for the drop target marker.
|
|
135
|
+
*/
|
|
136
|
+
_setupDropMarker() {
|
|
137
|
+
const editor = this.editor;
|
|
138
|
+
editor.ui.view.body.add(this._dropTargetLineView);
|
|
139
|
+
// Drop marker conversion for hovering over widgets.
|
|
140
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
141
|
+
model: 'drop-target',
|
|
142
|
+
view: {
|
|
143
|
+
classes: ['ck-clipboard-drop-target-range']
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// Drop marker conversion for in text and block drop target.
|
|
147
|
+
editor.conversion.for('editingDowncast').markerToElement({
|
|
148
|
+
model: 'drop-target',
|
|
149
|
+
view: (data, { writer }) => {
|
|
150
|
+
// Inline drop.
|
|
151
|
+
if (editor.model.schema.checkChild(data.markerRange.start, '$text')) {
|
|
152
|
+
this._dropTargetLineView.isVisible = false;
|
|
153
|
+
return this._createDropTargetPosition(writer);
|
|
154
|
+
}
|
|
155
|
+
// Block drop.
|
|
156
|
+
else {
|
|
157
|
+
if (data.markerRange.isCollapsed) {
|
|
158
|
+
this._updateDropTargetLine(data.markerRange);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this._dropTargetLineView.isVisible = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Updates the drop target marker to the provided range.
|
|
169
|
+
*
|
|
170
|
+
* @param targetRange The range to set the marker to.
|
|
171
|
+
*/
|
|
172
|
+
_updateDropMarker(targetRange) {
|
|
173
|
+
const editor = this.editor;
|
|
174
|
+
const markers = editor.model.markers;
|
|
175
|
+
editor.model.change(writer => {
|
|
176
|
+
if (markers.has('drop-target')) {
|
|
177
|
+
if (!markers.get('drop-target').getRange().isEqual(targetRange)) {
|
|
178
|
+
writer.updateMarker('drop-target', { range: targetRange });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
writer.addMarker('drop-target', {
|
|
183
|
+
range: targetRange,
|
|
184
|
+
usingOperation: false,
|
|
185
|
+
affectsData: false
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Creates the UI element for vertical (in-line) drop target.
|
|
192
|
+
*/
|
|
193
|
+
_createDropTargetPosition(writer) {
|
|
194
|
+
return writer.createUIElement('span', { class: 'ck ck-clipboard-drop-target-position' }, function (domDocument) {
|
|
195
|
+
const domElement = this.toDomElement(domDocument);
|
|
196
|
+
// Using word joiner to make this marker as high as text and also making text not break on marker.
|
|
197
|
+
domElement.append('\u2060', domDocument.createElement('span'), '\u2060');
|
|
198
|
+
return domElement;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Updates the horizontal drop target line.
|
|
203
|
+
*/
|
|
204
|
+
_updateDropTargetLine(range) {
|
|
205
|
+
const editing = this.editor.editing;
|
|
206
|
+
const nodeBefore = range.start.nodeBefore;
|
|
207
|
+
const nodeAfter = range.start.nodeAfter;
|
|
208
|
+
const nodeParent = range.start.parent;
|
|
209
|
+
const viewElementBefore = nodeBefore ? editing.mapper.toViewElement(nodeBefore) : null;
|
|
210
|
+
const domElementBefore = viewElementBefore ? editing.view.domConverter.mapViewToDom(viewElementBefore) : null;
|
|
211
|
+
const viewElementAfter = nodeAfter ? editing.mapper.toViewElement(nodeAfter) : null;
|
|
212
|
+
const domElementAfter = viewElementAfter ? editing.view.domConverter.mapViewToDom(viewElementAfter) : null;
|
|
213
|
+
const viewElementParent = editing.mapper.toViewElement(nodeParent);
|
|
214
|
+
if (!viewElementParent) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const domElementParent = editing.view.domConverter.mapViewToDom(viewElementParent);
|
|
218
|
+
const domScrollableRect = this._getScrollableRect(viewElementParent);
|
|
219
|
+
const { scrollX, scrollY } = global.window;
|
|
220
|
+
const rectBefore = domElementBefore ? new Rect(domElementBefore) : null;
|
|
221
|
+
const rectAfter = domElementAfter ? new Rect(domElementAfter) : null;
|
|
222
|
+
const rectParent = new Rect(domElementParent).excludeScrollbarsAndBorders();
|
|
223
|
+
const above = rectBefore ? rectBefore.bottom : rectParent.top;
|
|
224
|
+
const below = rectAfter ? rectAfter.top : rectParent.bottom;
|
|
225
|
+
const parentStyle = global.window.getComputedStyle(domElementParent);
|
|
226
|
+
const top = (above <= below ? (above + below) / 2 : below);
|
|
227
|
+
if (domScrollableRect.top < top && top < domScrollableRect.bottom) {
|
|
228
|
+
const left = rectParent.left + parseFloat(parentStyle.paddingLeft);
|
|
229
|
+
const right = rectParent.right - parseFloat(parentStyle.paddingRight);
|
|
230
|
+
const leftClamped = Math.max(left + scrollX, domScrollableRect.left);
|
|
231
|
+
const rightClamped = Math.min(right + scrollX, domScrollableRect.right);
|
|
232
|
+
this._dropTargetLineView.set({
|
|
233
|
+
isVisible: true,
|
|
234
|
+
left: leftClamped,
|
|
235
|
+
top: top + scrollY,
|
|
236
|
+
width: rightClamped - leftClamped
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this._dropTargetLineView.isVisible = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Finds the closest scrollable element rect for the given view element.
|
|
245
|
+
*/
|
|
246
|
+
_getScrollableRect(viewElement) {
|
|
247
|
+
const rootName = viewElement.root.rootName;
|
|
248
|
+
let domScrollable;
|
|
249
|
+
if (this._scrollables.has(rootName)) {
|
|
250
|
+
domScrollable = this._scrollables.get(rootName).domElement;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const domElement = this.editor.editing.view.domConverter.mapViewToDom(viewElement);
|
|
254
|
+
domScrollable = findScrollableElement(domElement);
|
|
255
|
+
this._domEmitter.listenTo(domScrollable, 'scroll', this._reconvertMarkerThrottled, { usePassive: true });
|
|
256
|
+
const resizeObserver = new ResizeObserver(domScrollable, this._reconvertMarkerThrottled);
|
|
257
|
+
this._scrollables.set(rootName, {
|
|
258
|
+
domElement: domScrollable,
|
|
259
|
+
resizeObserver
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return new Rect(domScrollable).excludeScrollbarsAndBorders();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Returns fixed selection range for given position and target element.
|
|
267
|
+
*/
|
|
268
|
+
function findDropTargetRange(editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
269
|
+
const model = editor.model;
|
|
270
|
+
const mapper = editor.editing.mapper;
|
|
271
|
+
const targetModelElement = getClosestMappedModelElement(editor, targetViewElement);
|
|
272
|
+
let modelElement = targetModelElement;
|
|
273
|
+
while (modelElement) {
|
|
274
|
+
if (!blockMode) {
|
|
275
|
+
if (model.schema.checkChild(modelElement, '$text')) {
|
|
276
|
+
if (targetViewRanges) {
|
|
277
|
+
const targetViewPosition = targetViewRanges[0].start;
|
|
278
|
+
const targetModelPosition = mapper.toModelPosition(targetViewPosition);
|
|
279
|
+
const canDropOnPosition = !draggedRange || Array
|
|
280
|
+
.from(draggedRange.getItems({ shallow: true }))
|
|
281
|
+
.some(item => model.schema.checkChild(targetModelPosition, item));
|
|
282
|
+
if (canDropOnPosition) {
|
|
283
|
+
if (model.schema.checkChild(targetModelPosition, '$text')) {
|
|
284
|
+
return model.createRange(targetModelPosition);
|
|
285
|
+
}
|
|
286
|
+
else if (targetViewPosition) {
|
|
287
|
+
// This is the case of dropping inside a span wrapper of an inline image.
|
|
288
|
+
return findDropTargetRangeForElement(editor, getClosestMappedModelElement(editor, targetViewPosition.parent), clientX, clientY);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (model.schema.isInline(modelElement)) {
|
|
294
|
+
return findDropTargetRangeForElement(editor, modelElement, clientX, clientY);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (model.schema.isBlock(modelElement)) {
|
|
298
|
+
return findDropTargetRangeForElement(editor, modelElement, clientX, clientY);
|
|
299
|
+
}
|
|
300
|
+
else if (model.schema.checkChild(modelElement, '$block')) {
|
|
301
|
+
const childNodes = Array.from(modelElement.getChildren())
|
|
302
|
+
.filter((node) => node.is('element') && !shouldIgnoreElement(editor, node));
|
|
303
|
+
let startIndex = 0;
|
|
304
|
+
let endIndex = childNodes.length;
|
|
305
|
+
if (endIndex == 0) {
|
|
306
|
+
return model.createRange(model.createPositionAt(modelElement, 'end'));
|
|
307
|
+
}
|
|
308
|
+
while (startIndex < endIndex - 1) {
|
|
309
|
+
const middleIndex = Math.floor((startIndex + endIndex) / 2);
|
|
310
|
+
const side = findElementSide(editor, childNodes[middleIndex], clientX, clientY);
|
|
311
|
+
if (side == 'before') {
|
|
312
|
+
endIndex = middleIndex;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
startIndex = middleIndex;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return findDropTargetRangeForElement(editor, childNodes[startIndex], clientX, clientY);
|
|
319
|
+
}
|
|
320
|
+
modelElement = modelElement.parent;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Returns true for elements which should be ignored.
|
|
326
|
+
*/
|
|
327
|
+
function shouldIgnoreElement(editor, modelElement) {
|
|
328
|
+
const mapper = editor.editing.mapper;
|
|
329
|
+
const domConverter = editor.editing.view.domConverter;
|
|
330
|
+
const viewElement = mapper.toViewElement(modelElement);
|
|
331
|
+
if (!viewElement) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
const domElement = domConverter.mapViewToDom(viewElement);
|
|
335
|
+
return global.window.getComputedStyle(domElement).float != 'none';
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Returns target range relative to the given element.
|
|
339
|
+
*/
|
|
340
|
+
function findDropTargetRangeForElement(editor, modelElement, clientX, clientY) {
|
|
341
|
+
const model = editor.model;
|
|
342
|
+
return model.createRange(model.createPositionAt(modelElement, findElementSide(editor, modelElement, clientX, clientY)));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Resolves whether drop marker should be before or after the given element.
|
|
346
|
+
*/
|
|
347
|
+
function findElementSide(editor, modelElement, clientX, clientY) {
|
|
348
|
+
const mapper = editor.editing.mapper;
|
|
349
|
+
const domConverter = editor.editing.view.domConverter;
|
|
350
|
+
const viewElement = mapper.toViewElement(modelElement);
|
|
351
|
+
const domElement = domConverter.mapViewToDom(viewElement);
|
|
352
|
+
const rect = new Rect(domElement);
|
|
353
|
+
if (editor.model.schema.isInline(modelElement)) {
|
|
354
|
+
return clientX < (rect.left + rect.right) / 2 ? 'before' : 'after';
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
return clientY < (rect.top + rect.bottom) / 2 ? 'before' : 'after';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Returns the closest model element for the specified view element.
|
|
362
|
+
*/
|
|
363
|
+
function getClosestMappedModelElement(editor, element) {
|
|
364
|
+
const mapper = editor.editing.mapper;
|
|
365
|
+
const view = editor.editing.view;
|
|
366
|
+
const targetModelElement = mapper.toModelElement(element);
|
|
367
|
+
if (targetModelElement) {
|
|
368
|
+
return targetModelElement;
|
|
369
|
+
}
|
|
370
|
+
// Find mapped ancestor if the target is inside not mapped element (for example inline code element).
|
|
371
|
+
const viewPosition = view.createPositionBefore(element);
|
|
372
|
+
const viewElement = mapper.findMappedViewAncestor(viewPosition);
|
|
373
|
+
return mapper.toModelElement(viewElement);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Returns the closest scrollable ancestor DOM element.
|
|
377
|
+
*
|
|
378
|
+
* It is assumed that `domNode` is attached to the document.
|
|
379
|
+
*/
|
|
380
|
+
function findScrollableElement(domNode) {
|
|
381
|
+
let domElement = domNode;
|
|
382
|
+
do {
|
|
383
|
+
domElement = domElement.parentElement;
|
|
384
|
+
const overflow = global.window.getComputedStyle(domElement).overflowY;
|
|
385
|
+
if (overflow == 'auto' || overflow == 'scroll') {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
} while (domElement.tagName != 'BODY');
|
|
389
|
+
return domElement;
|
|
390
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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 clipboard
|
|
7
|
+
*/
|
|
8
|
+
export { Clipboard } from './clipboard.js';
|
|
9
|
+
export { ClipboardPipeline } from './clipboardpipeline.js';
|
|
10
|
+
export { ClipboardMarkersUtils } from './clipboardmarkersutils.js';
|
|
11
|
+
export { plainTextToHtml } from './utils/plaintexttohtml.js';
|
|
12
|
+
export { viewToPlainText } from './utils/viewtoplaintext.js';
|
|
13
|
+
export { DragDrop } from './dragdrop.js';
|
|
14
|
+
export { PastePlainText } from './pasteplaintext.js';
|
|
15
|
+
export { DragDropTarget } from './dragdroptarget.js';
|
|
16
|
+
export { DragDropBlockToolbar } from './dragdropblocktoolbar.js';
|
|
17
|
+
export { ClipboardObserver } from './clipboardobserver.js';
|
|
18
|
+
export { DragDrop as _DragDrop } from './dragdrop.js';
|
|
19
|
+
export { DragDropBlockToolbar as _DragDropBlockToolbar } from './dragdropblocktoolbar.js';
|
|
20
|
+
export { DragDropTarget as _DragDropTarget } from './dragdroptarget.js';
|
|
21
|
+
export { LineView as _ClipboardLineView } from './lineview.js';
|
|
22
|
+
export { normalizeClipboardData as _normalizeClipboardData } from './utils/normalizeclipboarddata.js';
|
|
23
|
+
import './augmentation.js';
|
package/src/lineview.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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 clipboard/lineview
|
|
7
|
+
*/
|
|
8
|
+
/* istanbul ignore file -- @preserve */
|
|
9
|
+
import { View } from '@ckeditor/ckeditor5-ui';
|
|
10
|
+
import { toUnit } from '@ckeditor/ckeditor5-utils';
|
|
11
|
+
const toPx = /* #__PURE__ */ toUnit('px');
|
|
12
|
+
/**
|
|
13
|
+
* The horizontal drop target line view.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class LineView extends View {
|
|
18
|
+
/**
|
|
19
|
+
* @inheritDoc
|
|
20
|
+
*/
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
const bind = this.bindTemplate;
|
|
24
|
+
this.set({
|
|
25
|
+
isVisible: false,
|
|
26
|
+
left: null,
|
|
27
|
+
top: null,
|
|
28
|
+
width: null
|
|
29
|
+
});
|
|
30
|
+
this.setTemplate({
|
|
31
|
+
tag: 'div',
|
|
32
|
+
attributes: {
|
|
33
|
+
class: [
|
|
34
|
+
'ck',
|
|
35
|
+
'ck-clipboard-drop-target-line',
|
|
36
|
+
bind.if('isVisible', 'ck-hidden', value => !value)
|
|
37
|
+
],
|
|
38
|
+
style: {
|
|
39
|
+
left: bind.to('left', left => toPx(left)),
|
|
40
|
+
top: bind.to('top', top => toPx(top)),
|
|
41
|
+
width: bind.to('width', width => toPx(width))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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 clipboard/pasteplaintext
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import { ClipboardObserver } from './clipboardobserver.js';
|
|
10
|
+
import { ClipboardPipeline } from './clipboardpipeline.js';
|
|
11
|
+
/**
|
|
12
|
+
* The plugin detects the user's intention to paste plain text.
|
|
13
|
+
*
|
|
14
|
+
* For example, it detects the <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke.
|
|
15
|
+
*/
|
|
16
|
+
export class PastePlainText extends Plugin {
|
|
17
|
+
/**
|
|
18
|
+
* @inheritDoc
|
|
19
|
+
*/
|
|
20
|
+
static get pluginName() {
|
|
21
|
+
return 'PastePlainText';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @inheritDoc
|
|
25
|
+
*/
|
|
26
|
+
static get isOfficialPlugin() {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @inheritDoc
|
|
31
|
+
*/
|
|
32
|
+
static get requires() {
|
|
33
|
+
return [ClipboardPipeline];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @inheritDoc
|
|
37
|
+
*/
|
|
38
|
+
init() {
|
|
39
|
+
const editor = this.editor;
|
|
40
|
+
const model = editor.model;
|
|
41
|
+
const view = editor.editing.view;
|
|
42
|
+
const selection = model.document.selection;
|
|
43
|
+
view.addObserver(ClipboardObserver);
|
|
44
|
+
editor.plugins.get(ClipboardPipeline).on('contentInsertion', (evt, data) => {
|
|
45
|
+
if (!isUnformattedInlineContent(data.content, model)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
model.change(writer => {
|
|
49
|
+
// Formatting attributes should be preserved.
|
|
50
|
+
const textAttributes = Array.from(selection.getAttributes())
|
|
51
|
+
.filter(([key]) => model.schema.getAttributeProperties(key).isFormatting);
|
|
52
|
+
if (!selection.isCollapsed) {
|
|
53
|
+
model.deleteContent(selection, { doNotAutoparagraph: true });
|
|
54
|
+
}
|
|
55
|
+
// Also preserve other attributes if they survived the content deletion (because they were not fully selected).
|
|
56
|
+
// For example linkHref is not a formatting attribute but it should be preserved if pasted text was in the middle
|
|
57
|
+
// of a link.
|
|
58
|
+
textAttributes.push(...selection.getAttributes());
|
|
59
|
+
const range = writer.createRangeIn(data.content);
|
|
60
|
+
for (const item of range.getItems()) {
|
|
61
|
+
for (const attribute of textAttributes) {
|
|
62
|
+
if (model.schema.checkAttribute(item, attribute[0])) {
|
|
63
|
+
writer.setAttribute(attribute[0], attribute[1], item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns true if specified `documentFragment` represents the unformatted inline content.
|
|
73
|
+
*/
|
|
74
|
+
function isUnformattedInlineContent(documentFragment, model) {
|
|
75
|
+
let range = model.createRangeIn(documentFragment);
|
|
76
|
+
// We consider three scenarios here. The document fragment may include:
|
|
77
|
+
//
|
|
78
|
+
// 1. Only text and inline objects. Then it could be unformatted inline content.
|
|
79
|
+
// 2. Exactly one block element on top-level, eg. <p>Foobar</p> or <h2>Title</h2>.
|
|
80
|
+
// In this case, check this element content, it could be treated as unformatted inline content.
|
|
81
|
+
// 3. More block elements or block objects, then it is not unformatted inline content.
|
|
82
|
+
//
|
|
83
|
+
// We will check for scenario 2. specifically, and if it happens, we will unwrap it and follow with the regular algorithm.
|
|
84
|
+
//
|
|
85
|
+
if (documentFragment.childCount == 1) {
|
|
86
|
+
const child = documentFragment.getChild(0);
|
|
87
|
+
if (child.is('element') && model.schema.isBlock(child) && !model.schema.isObject(child) && !model.schema.isLimit(child)) {
|
|
88
|
+
// Scenario 2. as described above.
|
|
89
|
+
range = model.createRangeIn(child);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const child of range.getItems()) {
|
|
93
|
+
if (!model.schema.isInline(child)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const attributeKeys = Array.from(child.getAttributeKeys());
|
|
97
|
+
if (attributeKeys.find(key => model.schema.getAttributeProperties(key).isFormatting)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|