@ckeditor/ckeditor5-clipboard 0.0.0-nightly-20260108.0 → 0.0.0-nightly-next-20260108.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.
@@ -1,124 +0,0 @@
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
- }
@@ -1,390 +0,0 @@
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 DELETED
@@ -1,23 +0,0 @@
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 DELETED
@@ -1,46 +0,0 @@
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
- }
@@ -1,102 +0,0 @@
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
- }