@atlaskit/editor-plugin-block-controls 7.7.2 → 7.7.3
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/CHANGELOG.md +8 -0
- package/dist/cjs/blockControlsPlugin.js +14 -0
- package/dist/cjs/pm-plugins/selection-preservation/editor-commands.js +32 -0
- package/dist/cjs/pm-plugins/selection-preservation/plugin-key.js +8 -0
- package/dist/cjs/pm-plugins/selection-preservation/pm-plugin.js +99 -0
- package/dist/cjs/pm-plugins/selection-preservation/types.js +5 -0
- package/dist/cjs/pm-plugins/selection-preservation/utils.js +24 -0
- package/dist/cjs/ui/drag-handle.js +182 -62
- package/dist/es2019/blockControlsPlugin.js +11 -1
- package/dist/es2019/pm-plugins/selection-preservation/editor-commands.js +28 -0
- package/dist/es2019/pm-plugins/selection-preservation/plugin-key.js +2 -0
- package/dist/es2019/pm-plugins/selection-preservation/pm-plugin.js +92 -0
- package/dist/es2019/pm-plugins/selection-preservation/types.js +1 -0
- package/dist/es2019/pm-plugins/selection-preservation/utils.js +16 -0
- package/dist/es2019/ui/drag-handle.js +156 -33
- package/dist/esm/blockControlsPlugin.js +14 -0
- package/dist/esm/pm-plugins/selection-preservation/editor-commands.js +26 -0
- package/dist/esm/pm-plugins/selection-preservation/plugin-key.js +2 -0
- package/dist/esm/pm-plugins/selection-preservation/pm-plugin.js +93 -0
- package/dist/esm/pm-plugins/selection-preservation/types.js +1 -0
- package/dist/esm/pm-plugins/selection-preservation/utils.js +18 -0
- package/dist/esm/ui/drag-handle.js +184 -64
- package/dist/types/blockControlsPluginType.d.ts +12 -1
- package/dist/types/pm-plugins/selection-preservation/editor-commands.d.ts +13 -0
- package/dist/types/pm-plugins/selection-preservation/plugin-key.d.ts +3 -0
- package/dist/types/pm-plugins/selection-preservation/pm-plugin.d.ts +26 -0
- package/dist/types/pm-plugins/selection-preservation/types.d.ts +7 -0
- package/dist/types/pm-plugins/selection-preservation/utils.d.ts +10 -0
- package/dist/types-ts4.5/blockControlsPluginType.d.ts +12 -1
- package/dist/types-ts4.5/pm-plugins/selection-preservation/editor-commands.d.ts +13 -0
- package/dist/types-ts4.5/pm-plugins/selection-preservation/plugin-key.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/selection-preservation/pm-plugin.d.ts +26 -0
- package/dist/types-ts4.5/pm-plugins/selection-preservation/types.d.ts +7 -0
- package/dist/types-ts4.5/pm-plugins/selection-preservation/utils.d.ts +10 -0
- package/package.json +4 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { selectionPreservationPluginKey } from './plugin-key';
|
|
2
|
+
/**
|
|
3
|
+
* Start preserving the selection when a UI interaction requires it
|
|
4
|
+
*
|
|
5
|
+
* e.g., block menu open, drag-and-drop in progress
|
|
6
|
+
*/
|
|
7
|
+
export const startPreservingSelection = ({
|
|
8
|
+
tr
|
|
9
|
+
}) => {
|
|
10
|
+
const meta = {
|
|
11
|
+
type: 'startPreserving'
|
|
12
|
+
};
|
|
13
|
+
return tr.setMeta(selectionPreservationPluginKey, meta);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stop preserving the selection when a UI interaction completes
|
|
18
|
+
*
|
|
19
|
+
* e.g., block menu closed, drag-and-drop ended
|
|
20
|
+
*/
|
|
21
|
+
export const stopPreservingSelection = ({
|
|
22
|
+
tr
|
|
23
|
+
}) => {
|
|
24
|
+
const meta = {
|
|
25
|
+
type: 'stopPreserving'
|
|
26
|
+
};
|
|
27
|
+
return tr.setMeta(selectionPreservationPluginKey, meta);
|
|
28
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { logException } from '@atlaskit/editor-common/monitoring';
|
|
2
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { stopPreservingSelection } from './editor-commands';
|
|
5
|
+
import { selectionPreservationPluginKey } from './plugin-key';
|
|
6
|
+
import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Selection Preservation Plugin for ProseMirror Editor
|
|
10
|
+
*
|
|
11
|
+
* Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
|
|
12
|
+
* (like media/images). When a selection spans text + media nodes, subsequent transactions cause
|
|
13
|
+
* ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
|
|
14
|
+
* This is problematic for features like block menus and drag-and-drop that need stable multi-node
|
|
15
|
+
* selections while performing operations.
|
|
16
|
+
*
|
|
17
|
+
* The plugin works in three phases:
|
|
18
|
+
* (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
|
|
19
|
+
* (2) Map the saved selection through document changes to keep positions valid.
|
|
20
|
+
* (3) Detect when transactions collapse the selection and restore it via appendTransaction().
|
|
21
|
+
*
|
|
22
|
+
* Stops preserving via stopPreservingSelection() when the menu closes or operation completes.
|
|
23
|
+
*
|
|
24
|
+
* Commands: startPreservingSelection() to begin preservation, stopPreservingSelection() to end it.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: Only use when the UI blocks user selection changes. For example: when a block menu overlay
|
|
27
|
+
* is open (editor becomes non-interactive), during drag-and-drop operations (user is mid-drag), or
|
|
28
|
+
* when modal dialogs are active. In these states, any selection changes are from ProseMirror's
|
|
29
|
+
* internal behavior (not user input) and should be prevented. Do not use during normal editing.
|
|
30
|
+
*/
|
|
31
|
+
export const createSelectionPreservationPlugin = () => {
|
|
32
|
+
return new SafePlugin({
|
|
33
|
+
key: selectionPreservationPluginKey,
|
|
34
|
+
state: {
|
|
35
|
+
init() {
|
|
36
|
+
return {
|
|
37
|
+
preservedSelection: undefined
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
apply(tr, pluginState) {
|
|
41
|
+
const meta = getSelectionPreservationMeta(tr);
|
|
42
|
+
const newState = {
|
|
43
|
+
...pluginState
|
|
44
|
+
};
|
|
45
|
+
if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
|
|
46
|
+
newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
|
|
47
|
+
} else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
|
|
48
|
+
newState.preservedSelection = undefined;
|
|
49
|
+
}
|
|
50
|
+
if (newState.preservedSelection && tr.docChanged) {
|
|
51
|
+
const mapped = new TextSelection(newState.preservedSelection.$from, newState.preservedSelection.$to);
|
|
52
|
+
mapped.map(tr.doc, tr.mapping);
|
|
53
|
+
if (mapped.from >= 0 && mapped.to <= tr.doc.content.size && mapped.from !== mapped.to) {
|
|
54
|
+
newState.preservedSelection = mapped;
|
|
55
|
+
} else if (mapped.from === mapped.to) {
|
|
56
|
+
// If selection has collapsed to a cursor, e.g. after deleting the selection, stop preserving
|
|
57
|
+
newState.preservedSelection = undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return newState;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
64
|
+
const pluginState = selectionPreservationPluginKey.getState(newState);
|
|
65
|
+
const savedSel = pluginState === null || pluginState === void 0 ? void 0 : pluginState.preservedSelection;
|
|
66
|
+
if (!savedSel) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (hasUserSelectionChange(transactions)) {
|
|
70
|
+
// Auto-stop if user explicitly changes selection
|
|
71
|
+
return stopPreservingSelection({
|
|
72
|
+
tr: newState.tr
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const currSel = newState.selection;
|
|
76
|
+
const wasEmptySelection = savedSel.from === savedSel.to;
|
|
77
|
+
const selectionUnchanged = currSel.from === savedSel.from && currSel.to === savedSel.to;
|
|
78
|
+
const selectionInvalid = savedSel.from < 0 || savedSel.to > newState.doc.content.size;
|
|
79
|
+
if (wasEmptySelection || selectionUnchanged || selectionInvalid) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logException(error, {
|
|
86
|
+
location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { selectionPreservationPluginKey } from './plugin-key';
|
|
2
|
+
/**
|
|
3
|
+
* Detects if any of the transactions include user-driven selection changes.
|
|
4
|
+
*
|
|
5
|
+
* @param transactions The list of transactions to check.
|
|
6
|
+
* @returns True if any transaction includes a user-driven selection change, otherwise false.
|
|
7
|
+
*/
|
|
8
|
+
export const hasUserSelectionChange = transactions => {
|
|
9
|
+
return transactions.some(tr => tr.getMeta('pointer') || tr.getMeta('uiEvent') || tr.getMeta('paste') || tr.getMeta('cut') || tr.getMeta('composition') ||
|
|
10
|
+
// IME input
|
|
11
|
+
// Keyboard events that change selection
|
|
12
|
+
tr.getMeta('addToHistory') && tr.selectionSet);
|
|
13
|
+
};
|
|
14
|
+
export const getSelectionPreservationMeta = tr => {
|
|
15
|
+
return tr.getMeta(selectionPreservationPluginKey);
|
|
16
|
+
};
|
|
@@ -8,17 +8,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
8
8
|
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
|
|
9
9
|
import { css, jsx } from '@emotion/react';
|
|
10
10
|
import { bind } from 'bind-event-listener';
|
|
11
|
+
import { getDocument } from '@atlaskit/browser-apis';
|
|
11
12
|
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
12
13
|
import { browser as browserLegacy, getBrowserInfo } from '@atlaskit/editor-common/browser';
|
|
13
14
|
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
|
|
14
15
|
import { dragToMoveDown, dragToMoveLeft, dragToMoveRight, dragToMoveUp, getAriaKeyshortcuts, TooltipContentWithMultipleShortcuts } from '@atlaskit/editor-common/keymaps';
|
|
15
16
|
import { blockControlsMessages } from '@atlaskit/editor-common/messages';
|
|
16
17
|
import { deleteSelectedRange } from '@atlaskit/editor-common/selection';
|
|
17
|
-
import {
|
|
18
|
+
import { DRAG_HANDLE_WIDTH, tableControlsSpacing } from '@atlaskit/editor-common/styles';
|
|
18
19
|
import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector';
|
|
19
20
|
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
20
21
|
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
|
|
21
|
-
import {
|
|
22
|
+
import { akEditorFullPageNarrowBreakout, akEditorTableToolbarSize, relativeSizeToBaseFontSize } from '@atlaskit/editor-shared-styles/consts';
|
|
22
23
|
import DragHandleVerticalIcon from '@atlaskit/icon/core/drag-handle-vertical';
|
|
23
24
|
import DragHandlerIcon from '@atlaskit/icon/glyph/drag-handler';
|
|
24
25
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
@@ -32,6 +33,7 @@ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
|
32
33
|
import Tooltip from '@atlaskit/tooltip';
|
|
33
34
|
import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
|
|
34
35
|
import { key } from '../pm-plugins/main';
|
|
36
|
+
import { selectionPreservationPluginKey } from '../pm-plugins/selection-preservation/plugin-key';
|
|
35
37
|
import { getMultiSelectAnalyticsAttributes } from '../pm-plugins/utils/analytics';
|
|
36
38
|
import { getControlBottomCSSValue, getControlHeightCSSValue, getLeftPosition, getNodeHeight, getTopPosition, shouldBeSticky, shouldMaskNodeControls } from '../pm-plugins/utils/drag-handle-positions';
|
|
37
39
|
import { isHandleCorrelatedToSelection, isNodeWithCodeBlock, selectNode } from '../pm-plugins/utils/getSelection';
|
|
@@ -291,6 +293,71 @@ const getNodeMargins = node => {
|
|
|
291
293
|
}
|
|
292
294
|
return nodeMargins[nodeTypeName] || nodeMargins['default'];
|
|
293
295
|
};
|
|
296
|
+
const isRangeSpanningMultipleNodes = range => {
|
|
297
|
+
if (range.endIndex - range.startIndex <= 1) {
|
|
298
|
+
return false; // At most one child
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Count block nodes in the range, return true if more than one
|
|
302
|
+
let blockCount = 0;
|
|
303
|
+
for (let i = range.startIndex; i < range.endIndex; i++) {
|
|
304
|
+
if (range.parent.child(i).isBlock) {
|
|
305
|
+
blockCount++;
|
|
306
|
+
}
|
|
307
|
+
if (blockCount > 1) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
};
|
|
313
|
+
const shouldExpandSelection = (range, startPos) => {
|
|
314
|
+
return !!range && isRangeSpanningMultipleNodes(range) && range.start <= startPos && range.end >= startPos + 1;
|
|
315
|
+
};
|
|
316
|
+
const calculateBlockRange = ({
|
|
317
|
+
selection,
|
|
318
|
+
doc,
|
|
319
|
+
resolvedStartPos,
|
|
320
|
+
isShiftPressed
|
|
321
|
+
}) => {
|
|
322
|
+
if (!isShiftPressed) {
|
|
323
|
+
// When not pressing shift, create range including all block nodes within the selection
|
|
324
|
+
return selection.$from.blockRange(selection.$to);
|
|
325
|
+
}
|
|
326
|
+
if (resolvedStartPos.pos < selection.from) {
|
|
327
|
+
// If shift+click selecting upwards, get range from start of node to end of selection
|
|
328
|
+
return resolvedStartPos.blockRange(selection.$to);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Shift+click selecting downwards, get range from start of selection to pos within or after node
|
|
332
|
+
const resolvedPosWithinOrAfterNode = doc.resolve(resolvedStartPos.pos + 1);
|
|
333
|
+
return selection.$from.blockRange(resolvedPosWithinOrAfterNode);
|
|
334
|
+
};
|
|
335
|
+
const createExpandedSelection = (doc, selection, range) => {
|
|
336
|
+
return TextSelection.create(doc, Math.min(selection.from, range.start), Math.max(selection.to, range.end));
|
|
337
|
+
};
|
|
338
|
+
const createSelectionFromRange = ({
|
|
339
|
+
tr,
|
|
340
|
+
selection,
|
|
341
|
+
startPos,
|
|
342
|
+
nodeType,
|
|
343
|
+
range,
|
|
344
|
+
api
|
|
345
|
+
}) => {
|
|
346
|
+
if (range && shouldExpandSelection(range, startPos)) {
|
|
347
|
+
const expandedSelection = createExpandedSelection(tr.doc, selection, range);
|
|
348
|
+
if (!expandedSelection.eq(tr.selection)) {
|
|
349
|
+
tr.setSelection(expandedSelection);
|
|
350
|
+
}
|
|
351
|
+
return tr;
|
|
352
|
+
}
|
|
353
|
+
const node = tr.doc.nodeAt(startPos);
|
|
354
|
+
const isEmptyNode = (node === null || node === void 0 ? void 0 : node.content.size) === 0;
|
|
355
|
+
if (isEmptyNode && node.type.name !== 'paragraph') {
|
|
356
|
+
tr.setSelection(new NodeSelection(tr.doc.resolve(startPos)));
|
|
357
|
+
return tr;
|
|
358
|
+
}
|
|
359
|
+
return selectNode(tr, startPos, nodeType, api);
|
|
360
|
+
};
|
|
294
361
|
export const DragHandle = ({
|
|
295
362
|
view,
|
|
296
363
|
api,
|
|
@@ -302,7 +369,7 @@ export const DragHandle = ({
|
|
|
302
369
|
isTopLevelNode = true,
|
|
303
370
|
anchorRectCache
|
|
304
371
|
}) => {
|
|
305
|
-
var _api$
|
|
372
|
+
var _api$core4;
|
|
306
373
|
const buttonRef = useRef(null);
|
|
307
374
|
const [dragHandleSelected, setDragHandleSelected] = useState(false);
|
|
308
375
|
const [dragHandleDisabled, setDragHandleDisabled] = useState(false);
|
|
@@ -344,15 +411,71 @@ export const DragHandle = ({
|
|
|
344
411
|
}
|
|
345
412
|
}
|
|
346
413
|
}, [anchorName, nodeType, view.dom]);
|
|
347
|
-
const
|
|
414
|
+
const handleOnClickNew = useCallback(e => {
|
|
348
415
|
var _api$core;
|
|
416
|
+
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
|
|
417
|
+
tr
|
|
418
|
+
}) => {
|
|
419
|
+
var _api$analytics, _resolvedStartPos$nod, _selectionPreservatio, _api$blockControls, _api$blockControls2;
|
|
420
|
+
const startPos = getPos();
|
|
421
|
+
if (startPos === undefined) {
|
|
422
|
+
return tr;
|
|
423
|
+
}
|
|
424
|
+
const resolvedStartPos = tr.doc.resolve(startPos);
|
|
425
|
+
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
|
|
426
|
+
eventType: EVENT_TYPE.UI,
|
|
427
|
+
action: ACTION.CLICKED,
|
|
428
|
+
actionSubject: ACTION_SUBJECT.BUTTON,
|
|
429
|
+
actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
|
|
430
|
+
attributes: {
|
|
431
|
+
nodeDepth: resolvedStartPos.depth,
|
|
432
|
+
nodeType: ((_resolvedStartPos$nod = resolvedStartPos.nodeAfter) === null || _resolvedStartPos$nod === void 0 ? void 0 : _resolvedStartPos$nod.type.name) || ''
|
|
433
|
+
}
|
|
434
|
+
})(tr);
|
|
435
|
+
const preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
|
|
436
|
+
const selection = preservedSelection || tr.selection;
|
|
437
|
+
const range = calculateBlockRange({
|
|
438
|
+
doc: tr.doc,
|
|
439
|
+
selection,
|
|
440
|
+
resolvedStartPos,
|
|
441
|
+
isShiftPressed: e.shiftKey
|
|
442
|
+
});
|
|
443
|
+
tr = createSelectionFromRange({
|
|
444
|
+
tr,
|
|
445
|
+
selection,
|
|
446
|
+
startPos,
|
|
447
|
+
nodeType,
|
|
448
|
+
range,
|
|
449
|
+
api
|
|
450
|
+
});
|
|
451
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.startPreservingSelection()({
|
|
452
|
+
tr
|
|
453
|
+
});
|
|
454
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.toggleBlockMenu({
|
|
455
|
+
anchorName,
|
|
456
|
+
openedViaKeyboard: false,
|
|
457
|
+
triggerByNode: expValEqualsNoExposure('platform_synced_block', 'isEnabled', true) ? {
|
|
458
|
+
nodeType,
|
|
459
|
+
pos: startPos,
|
|
460
|
+
rootPos: tr.doc.resolve(startPos).before(1)
|
|
461
|
+
} : undefined
|
|
462
|
+
})({
|
|
463
|
+
tr
|
|
464
|
+
});
|
|
465
|
+
tr.setMeta('scrollIntoView', false);
|
|
466
|
+
return tr;
|
|
467
|
+
});
|
|
468
|
+
view.focus();
|
|
469
|
+
}, [api, view, getPos, nodeType, anchorName]);
|
|
470
|
+
const handleOnClick = useCallback(e => {
|
|
471
|
+
var _api$core2;
|
|
349
472
|
if (!isMultiSelect) {
|
|
350
473
|
setDragHandleSelected(!dragHandleSelected);
|
|
351
474
|
}
|
|
352
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
475
|
+
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
|
|
353
476
|
tr
|
|
354
477
|
}) => {
|
|
355
|
-
var _api$blockControls$sh, _api$
|
|
478
|
+
var _api$blockControls$sh, _api$analytics2;
|
|
356
479
|
const startPos = getPos();
|
|
357
480
|
if (startPos === undefined) {
|
|
358
481
|
return tr;
|
|
@@ -381,8 +504,8 @@ export const DragHandle = ({
|
|
|
381
504
|
rootPos
|
|
382
505
|
} : undefined;
|
|
383
506
|
if (BLOCK_MENU_ENABLED && editorExperiment('platform_editor_controls', 'variant1')) {
|
|
384
|
-
var _api$
|
|
385
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
507
|
+
var _api$blockControls3;
|
|
508
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.toggleBlockMenu({
|
|
386
509
|
anchorName,
|
|
387
510
|
triggerByNode,
|
|
388
511
|
openedViaKeyboard: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? false : undefined
|
|
@@ -391,8 +514,8 @@ export const DragHandle = ({
|
|
|
391
514
|
});
|
|
392
515
|
e.stopPropagation();
|
|
393
516
|
} else if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
|
|
394
|
-
var _api$
|
|
395
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
517
|
+
var _api$blockControls4;
|
|
518
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.toggleBlockMenu({
|
|
396
519
|
anchorName,
|
|
397
520
|
triggerByNode,
|
|
398
521
|
openedViaKeyboard: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? false : undefined
|
|
@@ -402,18 +525,18 @@ export const DragHandle = ({
|
|
|
402
525
|
e.stopPropagation();
|
|
403
526
|
}
|
|
404
527
|
} else if (isTopLevelNode && $anchor.depth <= DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH && e.shiftKey && fg('platform_editor_elements_dnd_shift_click_select')) {
|
|
405
|
-
var _api$
|
|
528
|
+
var _api$blockControls5;
|
|
406
529
|
const alignAnchorHeadToSel = alignAnchorHeadInDirectionOfPos(tr.selection, startPos);
|
|
407
530
|
const selectionWithExpandedHead = expandSelectionHeadToNodeAtPos(alignAnchorHeadToSel, startPos);
|
|
408
531
|
tr.setSelection(selectionWithExpandedHead);
|
|
409
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
532
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.setMultiSelectPositions()({
|
|
410
533
|
tr
|
|
411
534
|
});
|
|
412
535
|
}
|
|
413
536
|
const resolvedMovingNode = tr.doc.resolve(startPos);
|
|
414
537
|
const maybeNode = resolvedMovingNode.nodeAfter;
|
|
415
538
|
tr.setMeta('scrollIntoView', false);
|
|
416
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
539
|
+
api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.attachAnalyticsEvent({
|
|
417
540
|
eventType: EVENT_TYPE.UI,
|
|
418
541
|
action: ACTION.CLICKED,
|
|
419
542
|
actionSubject: ACTION_SUBJECT.BUTTON,
|
|
@@ -430,9 +553,9 @@ export const DragHandle = ({
|
|
|
430
553
|
const handleKeyDown = useCallback(e => {
|
|
431
554
|
// allow user to use spacebar to select the node
|
|
432
555
|
if (!e.repeat && e.key === ' ') {
|
|
433
|
-
var _api$
|
|
556
|
+
var _api$core3;
|
|
434
557
|
const startPos = getPos();
|
|
435
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
558
|
+
api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
|
|
436
559
|
tr
|
|
437
560
|
}) => {
|
|
438
561
|
if (startPos === undefined) {
|
|
@@ -455,21 +578,21 @@ export const DragHandle = ({
|
|
|
455
578
|
// return focus to editor to resume editing from caret position
|
|
456
579
|
view.focus();
|
|
457
580
|
}
|
|
458
|
-
}, [getPos, api === null || api === void 0 ? void 0 : (_api$
|
|
581
|
+
}, [getPos, api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions, isMultiSelect, view]);
|
|
459
582
|
const handleKeyDownNew = useCallback(e => {
|
|
460
583
|
// allow user to use spacebar to select the node
|
|
461
584
|
if (e.key === 'Enter' || !e.repeat && e.key === ' ') {
|
|
462
|
-
var _api$
|
|
463
|
-
if (
|
|
585
|
+
var _getDocument, _api$core5;
|
|
586
|
+
if (((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement) !== buttonRef.current) {
|
|
464
587
|
return;
|
|
465
588
|
}
|
|
466
589
|
e.preventDefault();
|
|
467
590
|
e.stopPropagation();
|
|
468
591
|
const startPos = getPos();
|
|
469
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
592
|
+
api === null || api === void 0 ? void 0 : (_api$core5 = api.core) === null || _api$core5 === void 0 ? void 0 : _api$core5.actions.execute(({
|
|
470
593
|
tr
|
|
471
594
|
}) => {
|
|
472
|
-
var _api$
|
|
595
|
+
var _api$blockControls6, _api$userIntent;
|
|
473
596
|
if (startPos === undefined) {
|
|
474
597
|
return tr;
|
|
475
598
|
}
|
|
@@ -483,7 +606,7 @@ export const DragHandle = ({
|
|
|
483
606
|
pos: startPos,
|
|
484
607
|
rootPos
|
|
485
608
|
} : undefined;
|
|
486
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
609
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.toggleBlockMenu({
|
|
487
610
|
anchorName,
|
|
488
611
|
triggerByNode,
|
|
489
612
|
openedViaKeyboard: true
|
|
@@ -501,9 +624,9 @@ export const DragHandle = ({
|
|
|
501
624
|
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
|
|
502
625
|
tr
|
|
503
626
|
}) => {
|
|
504
|
-
var _api$
|
|
627
|
+
var _api$blockControls7;
|
|
505
628
|
deleteSelectedRange(tr);
|
|
506
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
629
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls7 = api.blockControls) === null || _api$blockControls7 === void 0 ? void 0 : _api$blockControls7.commands.toggleBlockMenu({
|
|
507
630
|
closeMenu: true
|
|
508
631
|
})({
|
|
509
632
|
tr
|
|
@@ -532,8 +655,8 @@ export const DragHandle = ({
|
|
|
532
655
|
}) => {
|
|
533
656
|
var _api$blockControls$sh2;
|
|
534
657
|
if (isMultiSelect) {
|
|
535
|
-
var _api$
|
|
536
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
658
|
+
var _api$core6;
|
|
659
|
+
api === null || api === void 0 ? void 0 : (_api$core6 = api.core) === null || _api$core6 === void 0 ? void 0 : _api$core6.actions.execute(({
|
|
537
660
|
tr
|
|
538
661
|
}) => {
|
|
539
662
|
const handlePos = getPos();
|
|
@@ -542,8 +665,8 @@ export const DragHandle = ({
|
|
|
542
665
|
}
|
|
543
666
|
const newHandlePosCheck = isHandleCorrelatedToSelection(view.state, tr.selection, handlePos);
|
|
544
667
|
if (!tr.selection.empty && newHandlePosCheck) {
|
|
545
|
-
var _api$
|
|
546
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
668
|
+
var _api$blockControls8;
|
|
669
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls8 = api.blockControls) === null || _api$blockControls8 === void 0 ? void 0 : _api$blockControls8.commands.setMultiSelectPositions()({
|
|
547
670
|
tr
|
|
548
671
|
});
|
|
549
672
|
} else if (fg('platform_editor_elements_dnd_select_node_on_drag')) {
|
|
@@ -651,14 +774,14 @@ export const DragHandle = ({
|
|
|
651
774
|
});
|
|
652
775
|
},
|
|
653
776
|
onDragStart() {
|
|
654
|
-
var _api$
|
|
777
|
+
var _api$core7;
|
|
655
778
|
if (start === undefined) {
|
|
656
779
|
return;
|
|
657
780
|
}
|
|
658
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
781
|
+
api === null || api === void 0 ? void 0 : (_api$core7 = api.core) === null || _api$core7 === void 0 ? void 0 : _api$core7.actions.execute(({
|
|
659
782
|
tr
|
|
660
783
|
}) => {
|
|
661
|
-
var _api$blockControls$sh3, _api$
|
|
784
|
+
var _api$blockControls$sh3, _api$blockControls9, _api$analytics3;
|
|
662
785
|
let nodeTypes, hasSelectedMultipleNodes;
|
|
663
786
|
const resolvedMovingNode = tr.doc.resolve(start);
|
|
664
787
|
const maybeNode = resolvedMovingNode.nodeAfter;
|
|
@@ -671,11 +794,11 @@ export const DragHandle = ({
|
|
|
671
794
|
nodeTypes = maybeNode === null || maybeNode === void 0 ? void 0 : maybeNode.type.name;
|
|
672
795
|
hasSelectedMultipleNodes = false;
|
|
673
796
|
}
|
|
674
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
797
|
+
api === null || api === void 0 ? void 0 : (_api$blockControls9 = api.blockControls) === null || _api$blockControls9 === void 0 ? void 0 : _api$blockControls9.commands.setNodeDragged(getPos, anchorName, nodeType)({
|
|
675
798
|
tr
|
|
676
799
|
});
|
|
677
800
|
tr.setMeta('scrollIntoView', false);
|
|
678
|
-
api === null || api === void 0 ? void 0 : (_api$
|
|
801
|
+
api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.attachAnalyticsEvent({
|
|
679
802
|
eventType: EVENT_TYPE.UI,
|
|
680
803
|
action: ACTION.DRAGGED,
|
|
681
804
|
actionSubject: ACTION_SUBJECT.ELEMENT,
|
|
@@ -973,7 +1096,7 @@ export const DragHandle = ({
|
|
|
973
1096
|
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
|
|
974
1097
|
,
|
|
975
1098
|
style: !editorExperiment('platform_editor_controls', 'variant1') ? editorExperiment('platform_editor_block_control_optimise_render', true) ? positionStyles : positionStylesOld : {},
|
|
976
|
-
onClick: handleOnClick,
|
|
1099
|
+
onClick: expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true) ? handleOnClickNew : handleOnClick,
|
|
977
1100
|
onKeyDown: expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true) && expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? handleKeyDownNew : handleKeyDown
|
|
978
1101
|
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
|
|
979
1102
|
,
|
|
@@ -15,6 +15,8 @@ import { canMoveNodeUpOrDown } from './editor-commands/utils/move-node-utils';
|
|
|
15
15
|
import { firstNodeDecPlugin } from './pm-plugins/first-node-dec-plugin';
|
|
16
16
|
import { createInteractionTrackingPlugin, interactionTrackingPluginKey } from './pm-plugins/interaction-tracking/pm-plugin';
|
|
17
17
|
import { createPlugin, key } from './pm-plugins/main';
|
|
18
|
+
import { startPreservingSelection as _startPreservingSelection, stopPreservingSelection as _stopPreservingSelection } from './pm-plugins/selection-preservation/editor-commands';
|
|
19
|
+
import { createSelectionPreservationPlugin } from './pm-plugins/selection-preservation/pm-plugin';
|
|
18
20
|
import { selectNode } from './pm-plugins/utils/getSelection';
|
|
19
21
|
import BlockMenu from './ui/block-menu';
|
|
20
22
|
import { DragHandleMenu } from './ui/drag-handle-menu';
|
|
@@ -38,6 +40,12 @@ export var blockControlsPlugin = function blockControlsPlugin(_ref) {
|
|
|
38
40
|
plugin: createInteractionTrackingPlugin
|
|
39
41
|
});
|
|
40
42
|
}
|
|
43
|
+
if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
|
|
44
|
+
pmPlugins.push({
|
|
45
|
+
name: 'blockControlsSelectionPreservationPlugin',
|
|
46
|
+
plugin: createSelectionPreservationPlugin
|
|
47
|
+
});
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
// platform_editor_controls note: quick insert rendering fixes
|
|
43
51
|
if (areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar))) {
|
|
@@ -232,6 +240,12 @@ export var blockControlsPlugin = function blockControlsPlugin(_ref) {
|
|
|
232
240
|
},
|
|
233
241
|
moveNodeWithBlockMenu: function moveNodeWithBlockMenu(direction) {
|
|
234
242
|
return _moveNodeWithBlockMenu(api, direction);
|
|
243
|
+
},
|
|
244
|
+
startPreservingSelection: function startPreservingSelection() {
|
|
245
|
+
return _startPreservingSelection;
|
|
246
|
+
},
|
|
247
|
+
stopPreservingSelection: function stopPreservingSelection() {
|
|
248
|
+
return _stopPreservingSelection;
|
|
235
249
|
}
|
|
236
250
|
},
|
|
237
251
|
getSharedState: function getSharedState(editorState) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { selectionPreservationPluginKey } from './plugin-key';
|
|
2
|
+
/**
|
|
3
|
+
* Start preserving the selection when a UI interaction requires it
|
|
4
|
+
*
|
|
5
|
+
* e.g., block menu open, drag-and-drop in progress
|
|
6
|
+
*/
|
|
7
|
+
export var startPreservingSelection = function startPreservingSelection(_ref) {
|
|
8
|
+
var tr = _ref.tr;
|
|
9
|
+
var meta = {
|
|
10
|
+
type: 'startPreserving'
|
|
11
|
+
};
|
|
12
|
+
return tr.setMeta(selectionPreservationPluginKey, meta);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Stop preserving the selection when a UI interaction completes
|
|
17
|
+
*
|
|
18
|
+
* e.g., block menu closed, drag-and-drop ended
|
|
19
|
+
*/
|
|
20
|
+
export var stopPreservingSelection = function stopPreservingSelection(_ref2) {
|
|
21
|
+
var tr = _ref2.tr;
|
|
22
|
+
var meta = {
|
|
23
|
+
type: 'stopPreserving'
|
|
24
|
+
};
|
|
25
|
+
return tr.setMeta(selectionPreservationPluginKey, meta);
|
|
26
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
import { logException } from '@atlaskit/editor-common/monitoring';
|
|
5
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
6
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
7
|
+
import { stopPreservingSelection } from './editor-commands';
|
|
8
|
+
import { selectionPreservationPluginKey } from './plugin-key';
|
|
9
|
+
import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Selection Preservation Plugin for ProseMirror Editor
|
|
13
|
+
*
|
|
14
|
+
* Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
|
|
15
|
+
* (like media/images). When a selection spans text + media nodes, subsequent transactions cause
|
|
16
|
+
* ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
|
|
17
|
+
* This is problematic for features like block menus and drag-and-drop that need stable multi-node
|
|
18
|
+
* selections while performing operations.
|
|
19
|
+
*
|
|
20
|
+
* The plugin works in three phases:
|
|
21
|
+
* (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
|
|
22
|
+
* (2) Map the saved selection through document changes to keep positions valid.
|
|
23
|
+
* (3) Detect when transactions collapse the selection and restore it via appendTransaction().
|
|
24
|
+
*
|
|
25
|
+
* Stops preserving via stopPreservingSelection() when the menu closes or operation completes.
|
|
26
|
+
*
|
|
27
|
+
* Commands: startPreservingSelection() to begin preservation, stopPreservingSelection() to end it.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: Only use when the UI blocks user selection changes. For example: when a block menu overlay
|
|
30
|
+
* is open (editor becomes non-interactive), during drag-and-drop operations (user is mid-drag), or
|
|
31
|
+
* when modal dialogs are active. In these states, any selection changes are from ProseMirror's
|
|
32
|
+
* internal behavior (not user input) and should be prevented. Do not use during normal editing.
|
|
33
|
+
*/
|
|
34
|
+
export var createSelectionPreservationPlugin = function createSelectionPreservationPlugin() {
|
|
35
|
+
return new SafePlugin({
|
|
36
|
+
key: selectionPreservationPluginKey,
|
|
37
|
+
state: {
|
|
38
|
+
init: function init() {
|
|
39
|
+
return {
|
|
40
|
+
preservedSelection: undefined
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
apply: function apply(tr, pluginState) {
|
|
44
|
+
var meta = getSelectionPreservationMeta(tr);
|
|
45
|
+
var newState = _objectSpread({}, pluginState);
|
|
46
|
+
if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
|
|
47
|
+
newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
|
|
48
|
+
} else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
|
|
49
|
+
newState.preservedSelection = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (newState.preservedSelection && tr.docChanged) {
|
|
52
|
+
var mapped = new TextSelection(newState.preservedSelection.$from, newState.preservedSelection.$to);
|
|
53
|
+
mapped.map(tr.doc, tr.mapping);
|
|
54
|
+
if (mapped.from >= 0 && mapped.to <= tr.doc.content.size && mapped.from !== mapped.to) {
|
|
55
|
+
newState.preservedSelection = mapped;
|
|
56
|
+
} else if (mapped.from === mapped.to) {
|
|
57
|
+
// If selection has collapsed to a cursor, e.g. after deleting the selection, stop preserving
|
|
58
|
+
newState.preservedSelection = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return newState;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
|
|
65
|
+
var pluginState = selectionPreservationPluginKey.getState(newState);
|
|
66
|
+
var savedSel = pluginState === null || pluginState === void 0 ? void 0 : pluginState.preservedSelection;
|
|
67
|
+
if (!savedSel) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (hasUserSelectionChange(transactions)) {
|
|
71
|
+
// Auto-stop if user explicitly changes selection
|
|
72
|
+
return stopPreservingSelection({
|
|
73
|
+
tr: newState.tr
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
var currSel = newState.selection;
|
|
77
|
+
var wasEmptySelection = savedSel.from === savedSel.to;
|
|
78
|
+
var selectionUnchanged = currSel.from === savedSel.from && currSel.to === savedSel.to;
|
|
79
|
+
var selectionInvalid = savedSel.from < 0 || savedSel.to > newState.doc.content.size;
|
|
80
|
+
if (wasEmptySelection || selectionUnchanged || selectionInvalid) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logException(error, {
|
|
87
|
+
location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|