@atlaskit/editor-plugin-layout 13.2.2 → 13.2.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/layoutPlugin.js +3 -4
- package/dist/cjs/pm-plugins/actions.js +5 -5
- package/dist/cjs/pm-plugins/main.js +73 -7
- package/dist/cjs/pm-plugins/utils/index.js +193 -1
- package/dist/es2019/layoutPlugin.js +3 -4
- package/dist/es2019/pm-plugins/actions.js +5 -5
- package/dist/es2019/pm-plugins/main.js +72 -6
- package/dist/es2019/pm-plugins/utils/index.js +196 -0
- package/dist/esm/layoutPlugin.js +3 -4
- package/dist/esm/pm-plugins/actions.js +5 -5
- package/dist/esm/pm-plugins/main.js +75 -9
- package/dist/esm/pm-plugins/utils/index.js +192 -0
- package/dist/types/pm-plugins/utils/index.d.ts +19 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-layout
|
|
2
2
|
|
|
3
|
+
## 13.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`6827a39105ea0`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6827a39105ea0) -
|
|
8
|
+
EDITOR-7736: Fix left gap cursor before first image/expand in vertically-aligned layout column
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
|
|
3
11
|
## 13.2.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/cjs/layoutPlugin.js
CHANGED
|
@@ -16,7 +16,6 @@ var _state = require("@atlaskit/editor-prosemirror/state");
|
|
|
16
16
|
var _utils = require("@atlaskit/editor-prosemirror/utils");
|
|
17
17
|
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
18
18
|
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
|
|
19
|
-
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
|
|
20
19
|
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
|
|
21
20
|
var _actions = require("./pm-plugins/actions");
|
|
22
21
|
var _keymap = _interopRequireDefault(require("./pm-plugins/keymap"));
|
|
@@ -82,7 +81,7 @@ var layoutPlugin = exports.layoutPlugin = function layoutPlugin(_ref) {
|
|
|
82
81
|
}
|
|
83
82
|
}]);
|
|
84
83
|
}
|
|
85
|
-
if ((0,
|
|
84
|
+
if ((0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
86
85
|
var _api$uiControlRegistr;
|
|
87
86
|
api === null || api === void 0 || (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 || _api$uiControlRegistr.actions.register((0, _components.getLayoutColumnMenuComponents)({
|
|
88
87
|
api: api
|
|
@@ -107,7 +106,7 @@ var layoutPlugin = exports.layoutPlugin = function layoutPlugin(_ref) {
|
|
|
107
106
|
return (0, _main.default)(options, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
|
|
108
107
|
}
|
|
109
108
|
}];
|
|
110
|
-
if ((0,
|
|
109
|
+
if ((0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
111
110
|
plugins.push({
|
|
112
111
|
name: 'layoutKeymap',
|
|
113
112
|
plugin: function plugin() {
|
|
@@ -372,7 +371,7 @@ var layoutPlugin = exports.layoutPlugin = function layoutPlugin(_ref) {
|
|
|
372
371
|
popupsMountPoint = _ref5.popupsMountPoint,
|
|
373
372
|
popupsBoundariesElement = _ref5.popupsBoundariesElement,
|
|
374
373
|
popupsScrollableElement = _ref5.popupsScrollableElement;
|
|
375
|
-
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, (0, _experiments.editorExperiment)('advanced_layouts', true) ? /*#__PURE__*/_react.default.createElement(_globalStyles.GlobalStylesWrapper, null) : null, (0,
|
|
374
|
+
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, (0, _experiments.editorExperiment)('advanced_layouts', true) ? /*#__PURE__*/_react.default.createElement(_globalStyles.GlobalStylesWrapper, null) : null, (0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true) && editorView ? /*#__PURE__*/_react.default.createElement(_LayoutColumnMenu.LayoutColumnMenu, {
|
|
376
375
|
api: api,
|
|
377
376
|
editorView: editorView,
|
|
378
377
|
mountTo: popupsMountPoint,
|
|
@@ -17,7 +17,7 @@ var _state = require("@atlaskit/editor-prosemirror/state");
|
|
|
17
17
|
var _transform = require("@atlaskit/editor-prosemirror/transform");
|
|
18
18
|
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
|
|
19
19
|
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
20
|
-
var
|
|
20
|
+
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
|
|
21
21
|
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
|
|
22
22
|
var _consts = require("./consts");
|
|
23
23
|
var _pluginKey = require("./plugin-key");
|
|
@@ -596,7 +596,7 @@ var insertLayoutColumnAt = function insertLayoutColumnAt(side, editorAnalyticsAP
|
|
|
596
596
|
var inputMethod = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _analytics.INPUT_METHOD.LAYOUT_COLUMN_MENU;
|
|
597
597
|
return function (_ref5) {
|
|
598
598
|
var tr = _ref5.tr;
|
|
599
|
-
if (!(0,
|
|
599
|
+
if (!(0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
600
600
|
return null;
|
|
601
601
|
}
|
|
602
602
|
var selectedLayoutColumnsResult = (0, _layoutColumnSelection.getLayoutColumnsFromContentSelection)(tr.selection);
|
|
@@ -692,7 +692,7 @@ var setLayoutColumnValign = exports.setLayoutColumnValign = function setLayoutCo
|
|
|
692
692
|
inputMethod = _ref8$inputMethod === void 0 ? _analytics.INPUT_METHOD.LAYOUT_COLUMN_MENU : _ref8$inputMethod;
|
|
693
693
|
return function (_ref9) {
|
|
694
694
|
var tr = _ref9.tr;
|
|
695
|
-
if (!(0,
|
|
695
|
+
if (!(0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
696
696
|
return null;
|
|
697
697
|
}
|
|
698
698
|
var selectedLayoutColumnsResult = (0, _layoutColumnSelection.getSelectedLayoutColumnsFromSelection)(tr.selection);
|
|
@@ -749,7 +749,7 @@ var distributeLayoutColumns = exports.distributeLayoutColumns = function distrib
|
|
|
749
749
|
target = _ref10$target === void 0 ? 'selectedColumns' : _ref10$target;
|
|
750
750
|
return function (_ref11) {
|
|
751
751
|
var tr = _ref11.tr;
|
|
752
|
-
if (!(0,
|
|
752
|
+
if (!(0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
753
753
|
return null;
|
|
754
754
|
}
|
|
755
755
|
var selectedLayoutColumnsResult = target === 'allColumns' ? (0, _layoutColumnSelection.getAllLayoutColumnsFromSelection)(tr.selection) : (0, _layoutColumnSelection.getSelectedLayoutColumnsFromSelection)(tr.selection);
|
|
@@ -860,7 +860,7 @@ var deleteLayoutColumn = exports.deleteLayoutColumn = function deleteLayoutColum
|
|
|
860
860
|
return function (_ref18) {
|
|
861
861
|
var _api$blockControls4;
|
|
862
862
|
var tr = _ref18.tr;
|
|
863
|
-
if (!(0,
|
|
863
|
+
if (!(0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
864
864
|
return null;
|
|
865
865
|
}
|
|
866
866
|
var selectedLayoutColumnsResult = (0, _layoutColumnSelection.getLayoutColumnsFromContentSelection)(tr.selection);
|
|
@@ -16,6 +16,7 @@ var _model = require("@atlaskit/editor-prosemirror/model");
|
|
|
16
16
|
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
17
17
|
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
|
|
18
18
|
var _view2 = require("@atlaskit/editor-prosemirror/view");
|
|
19
|
+
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
|
|
19
20
|
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
|
|
20
21
|
var _actions = require("./actions");
|
|
21
22
|
var _columnResizeDivider = require("./column-resize-divider");
|
|
@@ -27,6 +28,33 @@ var _layoutColumnSelection = require("./utils/layout-column-selection");
|
|
|
27
28
|
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; }
|
|
28
29
|
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) { (0, _defineProperty2.default)(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; }
|
|
29
30
|
var DEFAULT_LAYOUT = exports.DEFAULT_LAYOUT = 'two_equal';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Shared blank-space gap cursor placement, used by both `handleClick` and `handleClickOn`
|
|
34
|
+
* (the latter catches clicks on atomic node views that stop propagation before `handleClick`).
|
|
35
|
+
* Returns `true` when it placed a selection and consumed the click, else `false`.
|
|
36
|
+
*/
|
|
37
|
+
var applyBlankSpaceGapCursor = function applyBlankSpaceGapCursor(view, event) {
|
|
38
|
+
if (!(0, _expValEquals.expValEquals)('platform_editor_layout_column_menu', 'isEnabled', true) || !(0, _experiments.editorExperiment)('advanced_layouts', true)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
var gapTarget = (0, _utils3.getGapCursorTargetForBlankSpaceClick)(view, event);
|
|
42
|
+
if (gapTarget === undefined) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
var $pos = view.state.doc.resolve(gapTarget.pos);
|
|
46
|
+
// A paragraph child takes a TextSelection (caret at the edge) rather than a gap cursor.
|
|
47
|
+
var isParagraphTarget = (0, _utils3.isParagraphBlankSpaceTarget)(view, gapTarget);
|
|
48
|
+
var nextSelection = isParagraphTarget ? _state.TextSelection.near($pos, gapTarget.side === 'left' ? 1 : -1) : new _selection.GapCursorSelection($pos, gapTarget.side === 'left' ? _selection.Side.LEFT : _selection.Side.RIGHT);
|
|
49
|
+
// Idempotency guard: `mousedown` already placed this selection, but the browser still
|
|
50
|
+
// fires `mouseup`, so `handleClick`/`handleClickOn` re-run for the same click. Consume it
|
|
51
|
+
// without re-dispatching (which would add a redundant undo entry).
|
|
52
|
+
if (view.state.selection.eq(nextSelection)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
view.dispatch(view.state.tr.setSelection(nextSelection).scrollIntoView());
|
|
56
|
+
return true;
|
|
57
|
+
};
|
|
30
58
|
var isWholeSelectionInsideLayoutColumn = function isWholeSelectionInsideLayoutColumn(state) {
|
|
31
59
|
// Since findParentNodeOfType doesn't check if selection.to shares the parent, we do this check ourselves
|
|
32
60
|
var fromParent = (0, _utils2.findParentNodeOfType)(state.schema.nodes.layoutColumn)(state.selection);
|
|
@@ -272,14 +300,52 @@ var _default = exports.default = function _default(options, editorAnalyticsAPI)
|
|
|
272
300
|
Backspace: handleDeleteLayoutColumn,
|
|
273
301
|
Delete: handleDeleteLayoutColumn
|
|
274
302
|
}),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
303
|
+
handleDOMEvents: {
|
|
304
|
+
// Place the gap cursor on `mousedown` (not `mouseup`) so the caret never flashes
|
|
305
|
+
// inside a nested editable child first.
|
|
306
|
+
mousedown: function mousedown(view, event) {
|
|
307
|
+
var target = event.target;
|
|
308
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (applyBlankSpaceGapCursor(view, event)) {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
// `preventDefault()` blocks the editor focus that makes the gap cursor blink,
|
|
314
|
+
// so restore it here. The `handleClick`/`handleClickOn` paths don't need this.
|
|
315
|
+
if (!view.hasFocus()) {
|
|
316
|
+
view.focus();
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
handleClickOn: function () {
|
|
324
|
+
var selectionClickHandler = (0, _selection.createSelectionClickHandler)(['layoutColumn'], function (target) {
|
|
325
|
+
return target.hasAttribute('data-layout-section') || target.hasAttribute('data-layout-column');
|
|
326
|
+
}, {
|
|
327
|
+
useLongPressSelection: options.useLongPressSelection || false,
|
|
328
|
+
getNodeSelectionPos: function getNodeSelectionPos(state, nodePos) {
|
|
329
|
+
return state.doc.resolve(nodePos).before();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return function (view, pos, node, nodePos, event, direct) {
|
|
333
|
+
// Fallback for clicks on an atomic node view that the mousedown hook missed.
|
|
334
|
+
var target = event.target;
|
|
335
|
+
if (!(target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) && applyBlankSpaceGapCursor(view, event)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return selectionClickHandler(view, pos, node, nodePos, event, direct);
|
|
339
|
+
};
|
|
340
|
+
}(),
|
|
341
|
+
handleClick: function handleClick(view, _pos, event) {
|
|
342
|
+
// Fallback for clicks the mousedown interceptor missed.
|
|
343
|
+
var target = event.target;
|
|
344
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
345
|
+
return false;
|
|
281
346
|
}
|
|
282
|
-
|
|
347
|
+
return applyBlankSpaceGapCursor(view, event);
|
|
348
|
+
}
|
|
283
349
|
},
|
|
284
350
|
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
|
|
285
351
|
var changes = [];
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.selectIntoLayout = exports.getMaybeLayoutSection = void 0;
|
|
6
|
+
exports.selectIntoLayout = exports.isParagraphBlankSpaceTarget = exports.getMaybeLayoutSection = exports.getGapCursorTargetForBlankSpaceClick = void 0;
|
|
7
7
|
var _selection = require("@atlaskit/editor-common/selection");
|
|
8
8
|
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
9
9
|
var _utils = require("@atlaskit/editor-prosemirror/utils");
|
|
10
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
10
11
|
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
|
|
11
12
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
12
13
|
|
|
@@ -56,4 +57,195 @@ var selectIntoLayout = exports.selectIntoLayout = function selectIntoLayout(view
|
|
|
56
57
|
}
|
|
57
58
|
return tr;
|
|
58
59
|
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* For a blank-space click inside a layout column — above the first child (middle/bottom-aligned
|
|
63
|
+
* columns) or below the last child (any alignment) — return the ProseMirror position and side
|
|
64
|
+
* for a gap cursor. Returns `undefined` when the kill switch is ON, the click is outside a
|
|
65
|
+
* layoutColumn, or the Y coordinate is not in blank space.
|
|
66
|
+
*
|
|
67
|
+
* The `advanced_layouts` / `platform_editor_layout_column_menu` gates live in the caller
|
|
68
|
+
* (`applyBlankSpaceGapCursor`); only the kill switch is checked here.
|
|
69
|
+
*/
|
|
70
|
+
var getGapCursorTargetForBlankSpaceClick = exports.getGapCursorTargetForBlankSpaceClick = function getGapCursorTargetForBlankSpaceClick(view, event) {
|
|
71
|
+
var _columnNode$attrs;
|
|
72
|
+
if ((0, _platformFeatureFlags.fg)('platform_editor_layout_column_menu_kill_switch_1')) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Resolve the column from the DOM target so it works even when posAtCoords returns null
|
|
77
|
+
// (nothing rendered at the clicked Y).
|
|
78
|
+
var target = event.target;
|
|
79
|
+
var columnEl = target === null || target === void 0 ? void 0 : target.closest('[data-layout-column]');
|
|
80
|
+
if (!columnEl) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
var columnStartPos;
|
|
84
|
+
try {
|
|
85
|
+
columnStartPos = view.posAtDOM(columnEl, 0);
|
|
86
|
+
} catch (_unused) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// posAtDOM resolves at varying depths, so walk up to find the layoutColumn.
|
|
91
|
+
var $columnStart = view.state.doc.resolve(columnStartPos);
|
|
92
|
+
var depth = -1;
|
|
93
|
+
for (var d = $columnStart.depth; d >= 0; d--) {
|
|
94
|
+
if ($columnStart.node(d).type.name === 'layoutColumn') {
|
|
95
|
+
depth = d;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (depth < 0) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
var columnNode = $columnStart.node(depth);
|
|
103
|
+
if (columnNode.childCount === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
var columnContentStart = $columnStart.start(depth);
|
|
107
|
+
var columnEndPos = $columnStart.end(depth);
|
|
108
|
+
var getChildDom = function getChildDom(nodePos) {
|
|
109
|
+
try {
|
|
110
|
+
var dom = view.nodeDOM(nodePos);
|
|
111
|
+
return dom instanceof Element ? dom : null;
|
|
112
|
+
} catch (_unused2) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var valign = (_columnNode$attrs = columnNode.attrs) === null || _columnNode$attrs === void 0 ? void 0 : _columnNode$attrs.valign;
|
|
117
|
+
var isNonTopAligned = valign && valign !== 'top';
|
|
118
|
+
|
|
119
|
+
// Use the column rect (not child rects) for above/below detection: it stays stable as
|
|
120
|
+
// gap-cursor widgets shift child DOM positions between repeated clicks.
|
|
121
|
+
var columnRect = columnEl.getBoundingClientRect();
|
|
122
|
+
|
|
123
|
+
// Click ABOVE the first child (only for middle/bottom-aligned columns).
|
|
124
|
+
var firstChildPos = columnContentStart;
|
|
125
|
+
var firstChildDom = getChildDom(firstChildPos);
|
|
126
|
+
if (isNonTopAligned && firstChildDom) {
|
|
127
|
+
var rect = firstChildDom.getBoundingClientRect();
|
|
128
|
+
if (event.clientY < rect.top && event.clientY >= columnRect.top) {
|
|
129
|
+
return {
|
|
130
|
+
pos: firstChildPos,
|
|
131
|
+
side: 'left'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Click BELOW the last child (for any column alignment).
|
|
137
|
+
var lastChild = columnNode.lastChild;
|
|
138
|
+
var lastChildEndPos = columnEndPos;
|
|
139
|
+
var lastChildStartPos = lastChild ? lastChildEndPos - lastChild.nodeSize : columnContentStart;
|
|
140
|
+
var lastChildDom = lastChild ? getChildDom(lastChildStartPos) : null;
|
|
141
|
+
if (lastChild && lastChildDom) {
|
|
142
|
+
var _rect = lastChildDom.getBoundingClientRect();
|
|
143
|
+
if (event.clientY > _rect.bottom && event.clientY <= columnRect.bottom) {
|
|
144
|
+
return {
|
|
145
|
+
pos: lastChildEndPos,
|
|
146
|
+
side: 'right'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback: click lands ON a single atomic child that fills the column (mediaSingle/expand),
|
|
152
|
+
// so the above/below checks never fired.
|
|
153
|
+
if (columnNode.childCount === 1) {
|
|
154
|
+
var onlyChild = columnNode.firstChild;
|
|
155
|
+
// Exclude `panel`: its wrapper makes `view.nodeDOM` non-null and intercepts clicks, so the
|
|
156
|
+
// guard below would wrongly fire for in-panel blank-space clicks (which have their own
|
|
157
|
+
// native gap cursor).
|
|
158
|
+
if (onlyChild && onlyChild.type.name !== 'paragraph' && onlyChild.type.name !== 'panel') {
|
|
159
|
+
// Bail when the click is on the child's own content. For media the wrapper is full-width
|
|
160
|
+
// so test against the <img> rect; resolve it only for a direct mediaSingle child (else
|
|
161
|
+
// getContentRect could grab an image nested in an expand and break its toggle).
|
|
162
|
+
var contentRect = onlyChild.type.name === 'mediaSingle' ? getContentRect(firstChildDom) : null;
|
|
163
|
+
if (contentRect) {
|
|
164
|
+
var insideImage = event.clientX >= contentRect.left && event.clientX <= contentRect.right && event.clientY >= contentRect.top && event.clientY <= contentRect.bottom;
|
|
165
|
+
if (insideImage) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Other atomics: bail when posAtCoords resolves strictly inside the node range.
|
|
170
|
+
var coordPos = null;
|
|
171
|
+
try {
|
|
172
|
+
coordPos = view.posAtCoords({
|
|
173
|
+
left: event.clientX,
|
|
174
|
+
top: event.clientY
|
|
175
|
+
});
|
|
176
|
+
} catch (_unused3) {
|
|
177
|
+
coordPos = null;
|
|
178
|
+
}
|
|
179
|
+
if (coordPos && coordPos.pos > firstChildPos && coordPos.pos < lastChildEndPos) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fire when the child DOM is resolvable, or when it's null (media not yet loaded) but
|
|
185
|
+
// the click target is the column itself (no node view intercepted it).
|
|
186
|
+
var targetEl = event.target;
|
|
187
|
+
var targetIsColumn = targetEl === columnEl;
|
|
188
|
+
var shouldUseFallback = firstChildDom !== null || targetIsColumn;
|
|
189
|
+
if (shouldUseFallback) {
|
|
190
|
+
var side = getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, event.clientX, event.clientY);
|
|
191
|
+
return side === 'left' ? {
|
|
192
|
+
pos: firstChildPos,
|
|
193
|
+
side: 'left'
|
|
194
|
+
} : {
|
|
195
|
+
pos: lastChildEndPos,
|
|
196
|
+
side: 'right'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The tight `<img>` content rect (or `null`). The outer wrapper often fills the whole column
|
|
206
|
+
* width, so the `<img>` rect is needed to tell "beside the image" from "on the image".
|
|
207
|
+
*/
|
|
208
|
+
var getContentRect = function getContentRect(firstChildDom) {
|
|
209
|
+
var img = firstChildDom === null || firstChildDom === void 0 ? void 0 : firstChildDom.querySelector('img');
|
|
210
|
+
return img ? img.getBoundingClientRect() : null;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Which side of an atomic child a blank-space click belongs to. Prefers the tight content (image)
|
|
215
|
+
* rect when available — using its midpoint so it's direction-agnostic (handles RTL right-aligned
|
|
216
|
+
* images) — otherwise falls back to the column's vertical midpoint.
|
|
217
|
+
*/
|
|
218
|
+
var getGapCursorSideForBlankSpaceClick = function getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, clientX, clientY) {
|
|
219
|
+
var contentRect = getContentRect(firstChildDom);
|
|
220
|
+
if (contentRect) {
|
|
221
|
+
if (clientY < contentRect.top) {
|
|
222
|
+
return 'left';
|
|
223
|
+
}
|
|
224
|
+
if (clientY > contentRect.bottom) {
|
|
225
|
+
return 'right';
|
|
226
|
+
}
|
|
227
|
+
if (clientX < (contentRect.left + contentRect.right) / 2) {
|
|
228
|
+
return 'left';
|
|
229
|
+
}
|
|
230
|
+
return 'right';
|
|
231
|
+
}
|
|
232
|
+
var columnMidY = columnRect.top + columnRect.height / 2;
|
|
233
|
+
return clientY < columnMidY ? 'left' : 'right';
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* True when the blank-space click target child is a paragraph, so the caller uses a TextSelection
|
|
238
|
+
* instead of a gap cursor. LEFT inspects the first child, RIGHT the last child.
|
|
239
|
+
*/
|
|
240
|
+
var isParagraphBlankSpaceTarget = exports.isParagraphBlankSpaceTarget = function isParagraphBlankSpaceTarget(view, gapTarget) {
|
|
241
|
+
var pos = gapTarget.pos,
|
|
242
|
+
side = gapTarget.side;
|
|
243
|
+
var doc = view.state.doc;
|
|
244
|
+
try {
|
|
245
|
+
var $pos = doc.resolve(pos);
|
|
246
|
+
var childNode = side === 'left' ? $pos.nodeAfter : $pos.nodeBefore;
|
|
247
|
+
return (childNode === null || childNode === void 0 ? void 0 : childNode.type.name) === 'paragraph';
|
|
248
|
+
} catch (_unused4) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
59
251
|
};
|
|
@@ -9,7 +9,6 @@ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
|
9
9
|
import { findParentNode } from '@atlaskit/editor-prosemirror/utils';
|
|
10
10
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
11
11
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
12
|
-
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
13
12
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
14
13
|
import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn, distributeLayoutColumns, insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnDangerPreview, setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
|
|
15
14
|
import { default as createLayoutKeymapPlugin } from './pm-plugins/keymap';
|
|
@@ -74,7 +73,7 @@ export const layoutPlugin = ({
|
|
|
74
73
|
}
|
|
75
74
|
}]);
|
|
76
75
|
}
|
|
77
|
-
if (
|
|
76
|
+
if (expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
78
77
|
var _api$uiControlRegistr;
|
|
79
78
|
api === null || api === void 0 ? void 0 : (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 ? void 0 : _api$uiControlRegistr.actions.register(getLayoutColumnMenuComponents({
|
|
80
79
|
api
|
|
@@ -99,7 +98,7 @@ export const layoutPlugin = ({
|
|
|
99
98
|
return createLayoutPlugin(options, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
|
|
100
99
|
}
|
|
101
100
|
}];
|
|
102
|
-
if (
|
|
101
|
+
if (expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
103
102
|
plugins.push({
|
|
104
103
|
name: 'layoutKeymap',
|
|
105
104
|
plugin: () => createLayoutKeymapPlugin({
|
|
@@ -344,7 +343,7 @@ export const layoutPlugin = ({
|
|
|
344
343
|
popupsBoundariesElement,
|
|
345
344
|
popupsScrollableElement
|
|
346
345
|
}) {
|
|
347
|
-
return /*#__PURE__*/React.createElement(React.Fragment, null, editorExperiment('advanced_layouts', true) ? /*#__PURE__*/React.createElement(GlobalStylesWrapper, null) : null,
|
|
346
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, editorExperiment('advanced_layouts', true) ? /*#__PURE__*/React.createElement(GlobalStylesWrapper, null) : null, expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) && editorView ? /*#__PURE__*/React.createElement(LayoutColumnMenu, {
|
|
348
347
|
api: api,
|
|
349
348
|
editorView: editorView,
|
|
350
349
|
mountTo: popupsMountPoint,
|
|
@@ -6,7 +6,7 @@ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state
|
|
|
6
6
|
import { Mapping, StepMap } from '@atlaskit/editor-prosemirror/transform';
|
|
7
7
|
import { safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
8
8
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
9
|
-
import {
|
|
9
|
+
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
10
10
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
11
11
|
import { DEFAULT_LAYOUT_COLUMN_VALIGN, EVEN_DISTRIBUTED_COL_WIDTHS, MAX_LAYOUT_COLUMNS, MAX_STANDARD_LAYOUT_COLUMNS, MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
|
|
12
12
|
import { pluginKey } from './plugin-key';
|
|
@@ -568,7 +568,7 @@ export function getEffectiveMaxLayoutColumns() {
|
|
|
568
568
|
const insertLayoutColumnAt = (side, editorAnalyticsAPI, inputMethod = INPUT_METHOD.LAYOUT_COLUMN_MENU) => ({
|
|
569
569
|
tr
|
|
570
570
|
}) => {
|
|
571
|
-
if (!
|
|
571
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
572
572
|
return null;
|
|
573
573
|
}
|
|
574
574
|
const selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
|
|
@@ -664,7 +664,7 @@ export const setLayoutColumnValign = ({
|
|
|
664
664
|
}, editorAnalyticsAPI, api) => ({
|
|
665
665
|
tr
|
|
666
666
|
}) => {
|
|
667
|
-
if (!
|
|
667
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
668
668
|
return null;
|
|
669
669
|
}
|
|
670
670
|
const selectedLayoutColumnsResult = getSelectedLayoutColumnsFromSelection(tr.selection);
|
|
@@ -720,7 +720,7 @@ export const distributeLayoutColumns = (editorAnalyticsAPI, api) => ({
|
|
|
720
720
|
} = {}) => ({
|
|
721
721
|
tr
|
|
722
722
|
}) => {
|
|
723
|
-
if (!
|
|
723
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
724
724
|
return null;
|
|
725
725
|
}
|
|
726
726
|
const selectedLayoutColumnsResult = target === 'allColumns' ? getAllLayoutColumnsFromSelection(tr.selection) : getSelectedLayoutColumnsFromSelection(tr.selection);
|
|
@@ -826,7 +826,7 @@ export const deleteLayoutColumn = ({
|
|
|
826
826
|
tr
|
|
827
827
|
}) => {
|
|
828
828
|
var _api$blockControls4;
|
|
829
|
-
if (!
|
|
829
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
830
830
|
return null;
|
|
831
831
|
}
|
|
832
832
|
const selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
|
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
2
2
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
-
import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
|
|
3
|
+
import { createSelectionClickHandler, GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
|
|
4
4
|
import { filterCommand as filter } from '@atlaskit/editor-common/utils';
|
|
5
5
|
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
6
6
|
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
7
7
|
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
8
8
|
import { findParentNodeClosestToPos, findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
9
9
|
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
10
|
+
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
10
11
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
11
12
|
import { fixColumnSizes, fixColumnStructure, getSelectedLayout, LAYOUT_COLUMN_INSERT_META } from './actions';
|
|
12
13
|
import { getColumnDividerDecorations } from './column-resize-divider';
|
|
13
14
|
import { EVEN_DISTRIBUTED_COL_WIDTHS } from './consts';
|
|
14
15
|
import { pluginKey } from './plugin-key';
|
|
15
16
|
import { pluginKey as layoutResizingPluginKey } from './resizing';
|
|
16
|
-
import { getMaybeLayoutSection } from './utils';
|
|
17
|
+
import { getGapCursorTargetForBlankSpaceClick, getMaybeLayoutSection, isParagraphBlankSpaceTarget } from './utils';
|
|
17
18
|
import { getSelectedLayoutColumnsFromSelection } from './utils/layout-column-selection';
|
|
18
19
|
export const DEFAULT_LAYOUT = 'two_equal';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Shared blank-space gap cursor placement, used by both `handleClick` and `handleClickOn`
|
|
23
|
+
* (the latter catches clicks on atomic node views that stop propagation before `handleClick`).
|
|
24
|
+
* Returns `true` when it placed a selection and consumed the click, else `false`.
|
|
25
|
+
*/
|
|
26
|
+
const applyBlankSpaceGapCursor = (view, event) => {
|
|
27
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) || !editorExperiment('advanced_layouts', true)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const gapTarget = getGapCursorTargetForBlankSpaceClick(view, event);
|
|
31
|
+
if (gapTarget === undefined) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const $pos = view.state.doc.resolve(gapTarget.pos);
|
|
35
|
+
// A paragraph child takes a TextSelection (caret at the edge) rather than a gap cursor.
|
|
36
|
+
const isParagraphTarget = isParagraphBlankSpaceTarget(view, gapTarget);
|
|
37
|
+
const nextSelection = isParagraphTarget ? TextSelection.near($pos, gapTarget.side === 'left' ? 1 : -1) : new GapCursorSelection($pos, gapTarget.side === 'left' ? Side.LEFT : Side.RIGHT);
|
|
38
|
+
// Idempotency guard: `mousedown` already placed this selection, but the browser still
|
|
39
|
+
// fires `mouseup`, so `handleClick`/`handleClickOn` re-run for the same click. Consume it
|
|
40
|
+
// without re-dispatching (which would add a redundant undo entry).
|
|
41
|
+
if (view.state.selection.eq(nextSelection)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
view.dispatch(view.state.tr.setSelection(nextSelection).scrollIntoView());
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
19
47
|
const isWholeSelectionInsideLayoutColumn = state => {
|
|
20
48
|
// Since findParentNodeOfType doesn't check if selection.to shares the parent, we do this check ourselves
|
|
21
49
|
const fromParent = findParentNodeOfType(state.schema.nodes.layoutColumn)(state.selection);
|
|
@@ -272,10 +300,48 @@ export default ((options, editorAnalyticsAPI) => {
|
|
|
272
300
|
Backspace: handleDeleteLayoutColumn,
|
|
273
301
|
Delete: handleDeleteLayoutColumn
|
|
274
302
|
}),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
303
|
+
handleDOMEvents: {
|
|
304
|
+
// Place the gap cursor on `mousedown` (not `mouseup`) so the caret never flashes
|
|
305
|
+
// inside a nested editable child first.
|
|
306
|
+
mousedown(view, event) {
|
|
307
|
+
const target = event.target;
|
|
308
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (applyBlankSpaceGapCursor(view, event)) {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
// `preventDefault()` blocks the editor focus that makes the gap cursor blink,
|
|
314
|
+
// so restore it here. The `handleClick`/`handleClickOn` paths don't need this.
|
|
315
|
+
if (!view.hasFocus()) {
|
|
316
|
+
view.focus();
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
handleClickOn: (() => {
|
|
324
|
+
const selectionClickHandler = createSelectionClickHandler(['layoutColumn'], target => target.hasAttribute('data-layout-section') || target.hasAttribute('data-layout-column'), {
|
|
325
|
+
useLongPressSelection: options.useLongPressSelection || false,
|
|
326
|
+
getNodeSelectionPos: (state, nodePos) => state.doc.resolve(nodePos).before()
|
|
327
|
+
});
|
|
328
|
+
return (view, pos, node, nodePos, event, direct) => {
|
|
329
|
+
// Fallback for clicks on an atomic node view that the mousedown hook missed.
|
|
330
|
+
const target = event.target;
|
|
331
|
+
if (!(target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) && applyBlankSpaceGapCursor(view, event)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
return selectionClickHandler(view, pos, node, nodePos, event, direct);
|
|
335
|
+
};
|
|
336
|
+
})(),
|
|
337
|
+
handleClick(view, _pos, event) {
|
|
338
|
+
// Fallback for clicks the mousedown interceptor missed.
|
|
339
|
+
const target = event.target;
|
|
340
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return applyBlankSpaceGapCursor(view, event);
|
|
344
|
+
}
|
|
279
345
|
},
|
|
280
346
|
appendTransaction: (transactions, _oldState, newState) => {
|
|
281
347
|
const changes = [];
|
|
@@ -2,6 +2,7 @@ import { GapCursorSelection } from '@atlaskit/editor-common/selection';
|
|
|
2
2
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
3
3
|
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
4
|
import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
5
6
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
6
7
|
export const getMaybeLayoutSection = state => {
|
|
7
8
|
const {
|
|
@@ -53,4 +54,199 @@ export const selectIntoLayout = (view, posOfLayout, childIndex = 0) => {
|
|
|
53
54
|
}
|
|
54
55
|
return tr;
|
|
55
56
|
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* For a blank-space click inside a layout column — above the first child (middle/bottom-aligned
|
|
60
|
+
* columns) or below the last child (any alignment) — return the ProseMirror position and side
|
|
61
|
+
* for a gap cursor. Returns `undefined` when the kill switch is ON, the click is outside a
|
|
62
|
+
* layoutColumn, or the Y coordinate is not in blank space.
|
|
63
|
+
*
|
|
64
|
+
* The `advanced_layouts` / `platform_editor_layout_column_menu` gates live in the caller
|
|
65
|
+
* (`applyBlankSpaceGapCursor`); only the kill switch is checked here.
|
|
66
|
+
*/
|
|
67
|
+
export const getGapCursorTargetForBlankSpaceClick = (view, event) => {
|
|
68
|
+
var _columnNode$attrs;
|
|
69
|
+
if (fg('platform_editor_layout_column_menu_kill_switch_1')) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve the column from the DOM target so it works even when posAtCoords returns null
|
|
74
|
+
// (nothing rendered at the clicked Y).
|
|
75
|
+
const target = event.target;
|
|
76
|
+
const columnEl = target === null || target === void 0 ? void 0 : target.closest('[data-layout-column]');
|
|
77
|
+
if (!columnEl) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
let columnStartPos;
|
|
81
|
+
try {
|
|
82
|
+
columnStartPos = view.posAtDOM(columnEl, 0);
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// posAtDOM resolves at varying depths, so walk up to find the layoutColumn.
|
|
88
|
+
const $columnStart = view.state.doc.resolve(columnStartPos);
|
|
89
|
+
let depth = -1;
|
|
90
|
+
for (let d = $columnStart.depth; d >= 0; d--) {
|
|
91
|
+
if ($columnStart.node(d).type.name === 'layoutColumn') {
|
|
92
|
+
depth = d;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (depth < 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const columnNode = $columnStart.node(depth);
|
|
100
|
+
if (columnNode.childCount === 0) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const columnContentStart = $columnStart.start(depth);
|
|
104
|
+
const columnEndPos = $columnStart.end(depth);
|
|
105
|
+
const getChildDom = nodePos => {
|
|
106
|
+
try {
|
|
107
|
+
const dom = view.nodeDOM(nodePos);
|
|
108
|
+
return dom instanceof Element ? dom : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const valign = (_columnNode$attrs = columnNode.attrs) === null || _columnNode$attrs === void 0 ? void 0 : _columnNode$attrs.valign;
|
|
114
|
+
const isNonTopAligned = valign && valign !== 'top';
|
|
115
|
+
|
|
116
|
+
// Use the column rect (not child rects) for above/below detection: it stays stable as
|
|
117
|
+
// gap-cursor widgets shift child DOM positions between repeated clicks.
|
|
118
|
+
const columnRect = columnEl.getBoundingClientRect();
|
|
119
|
+
|
|
120
|
+
// Click ABOVE the first child (only for middle/bottom-aligned columns).
|
|
121
|
+
const firstChildPos = columnContentStart;
|
|
122
|
+
const firstChildDom = getChildDom(firstChildPos);
|
|
123
|
+
if (isNonTopAligned && firstChildDom) {
|
|
124
|
+
const rect = firstChildDom.getBoundingClientRect();
|
|
125
|
+
if (event.clientY < rect.top && event.clientY >= columnRect.top) {
|
|
126
|
+
return {
|
|
127
|
+
pos: firstChildPos,
|
|
128
|
+
side: 'left'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Click BELOW the last child (for any column alignment).
|
|
134
|
+
const lastChild = columnNode.lastChild;
|
|
135
|
+
const lastChildEndPos = columnEndPos;
|
|
136
|
+
const lastChildStartPos = lastChild ? lastChildEndPos - lastChild.nodeSize : columnContentStart;
|
|
137
|
+
const lastChildDom = lastChild ? getChildDom(lastChildStartPos) : null;
|
|
138
|
+
if (lastChild && lastChildDom) {
|
|
139
|
+
const rect = lastChildDom.getBoundingClientRect();
|
|
140
|
+
if (event.clientY > rect.bottom && event.clientY <= columnRect.bottom) {
|
|
141
|
+
return {
|
|
142
|
+
pos: lastChildEndPos,
|
|
143
|
+
side: 'right'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: click lands ON a single atomic child that fills the column (mediaSingle/expand),
|
|
149
|
+
// so the above/below checks never fired.
|
|
150
|
+
if (columnNode.childCount === 1) {
|
|
151
|
+
const onlyChild = columnNode.firstChild;
|
|
152
|
+
// Exclude `panel`: its wrapper makes `view.nodeDOM` non-null and intercepts clicks, so the
|
|
153
|
+
// guard below would wrongly fire for in-panel blank-space clicks (which have their own
|
|
154
|
+
// native gap cursor).
|
|
155
|
+
if (onlyChild && onlyChild.type.name !== 'paragraph' && onlyChild.type.name !== 'panel') {
|
|
156
|
+
// Bail when the click is on the child's own content. For media the wrapper is full-width
|
|
157
|
+
// so test against the <img> rect; resolve it only for a direct mediaSingle child (else
|
|
158
|
+
// getContentRect could grab an image nested in an expand and break its toggle).
|
|
159
|
+
const contentRect = onlyChild.type.name === 'mediaSingle' ? getContentRect(firstChildDom) : null;
|
|
160
|
+
if (contentRect) {
|
|
161
|
+
const insideImage = event.clientX >= contentRect.left && event.clientX <= contentRect.right && event.clientY >= contentRect.top && event.clientY <= contentRect.bottom;
|
|
162
|
+
if (insideImage) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Other atomics: bail when posAtCoords resolves strictly inside the node range.
|
|
167
|
+
let coordPos = null;
|
|
168
|
+
try {
|
|
169
|
+
coordPos = view.posAtCoords({
|
|
170
|
+
left: event.clientX,
|
|
171
|
+
top: event.clientY
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
coordPos = null;
|
|
175
|
+
}
|
|
176
|
+
if (coordPos && coordPos.pos > firstChildPos && coordPos.pos < lastChildEndPos) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fire when the child DOM is resolvable, or when it's null (media not yet loaded) but
|
|
182
|
+
// the click target is the column itself (no node view intercepted it).
|
|
183
|
+
const targetEl = event.target;
|
|
184
|
+
const targetIsColumn = targetEl === columnEl;
|
|
185
|
+
const shouldUseFallback = firstChildDom !== null || targetIsColumn;
|
|
186
|
+
if (shouldUseFallback) {
|
|
187
|
+
const side = getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, event.clientX, event.clientY);
|
|
188
|
+
return side === 'left' ? {
|
|
189
|
+
pos: firstChildPos,
|
|
190
|
+
side: 'left'
|
|
191
|
+
} : {
|
|
192
|
+
pos: lastChildEndPos,
|
|
193
|
+
side: 'right'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The tight `<img>` content rect (or `null`). The outer wrapper often fills the whole column
|
|
203
|
+
* width, so the `<img>` rect is needed to tell "beside the image" from "on the image".
|
|
204
|
+
*/
|
|
205
|
+
const getContentRect = firstChildDom => {
|
|
206
|
+
const img = firstChildDom === null || firstChildDom === void 0 ? void 0 : firstChildDom.querySelector('img');
|
|
207
|
+
return img ? img.getBoundingClientRect() : null;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Which side of an atomic child a blank-space click belongs to. Prefers the tight content (image)
|
|
212
|
+
* rect when available — using its midpoint so it's direction-agnostic (handles RTL right-aligned
|
|
213
|
+
* images) — otherwise falls back to the column's vertical midpoint.
|
|
214
|
+
*/
|
|
215
|
+
const getGapCursorSideForBlankSpaceClick = (firstChildDom, columnRect, clientX, clientY) => {
|
|
216
|
+
const contentRect = getContentRect(firstChildDom);
|
|
217
|
+
if (contentRect) {
|
|
218
|
+
if (clientY < contentRect.top) {
|
|
219
|
+
return 'left';
|
|
220
|
+
}
|
|
221
|
+
if (clientY > contentRect.bottom) {
|
|
222
|
+
return 'right';
|
|
223
|
+
}
|
|
224
|
+
if (clientX < (contentRect.left + contentRect.right) / 2) {
|
|
225
|
+
return 'left';
|
|
226
|
+
}
|
|
227
|
+
return 'right';
|
|
228
|
+
}
|
|
229
|
+
const columnMidY = columnRect.top + columnRect.height / 2;
|
|
230
|
+
return clientY < columnMidY ? 'left' : 'right';
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* True when the blank-space click target child is a paragraph, so the caller uses a TextSelection
|
|
235
|
+
* instead of a gap cursor. LEFT inspects the first child, RIGHT the last child.
|
|
236
|
+
*/
|
|
237
|
+
export const isParagraphBlankSpaceTarget = (view, gapTarget) => {
|
|
238
|
+
const {
|
|
239
|
+
pos,
|
|
240
|
+
side
|
|
241
|
+
} = gapTarget;
|
|
242
|
+
const {
|
|
243
|
+
doc
|
|
244
|
+
} = view.state;
|
|
245
|
+
try {
|
|
246
|
+
const $pos = doc.resolve(pos);
|
|
247
|
+
const childNode = side === 'left' ? $pos.nodeAfter : $pos.nodeBefore;
|
|
248
|
+
return (childNode === null || childNode === void 0 ? void 0 : childNode.type.name) === 'paragraph';
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
56
252
|
};
|
package/dist/esm/layoutPlugin.js
CHANGED
|
@@ -9,7 +9,6 @@ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
|
9
9
|
import { findParentNode } from '@atlaskit/editor-prosemirror/utils';
|
|
10
10
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
11
11
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
12
|
-
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
13
12
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
14
13
|
import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn as _deleteLayoutColumn, distributeLayoutColumns as _distributeLayoutColumns, insertLayoutColumn as _insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnDangerPreview, setLayoutColumnValign as _setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
|
|
15
14
|
import { default as createLayoutKeymapPlugin } from './pm-plugins/keymap';
|
|
@@ -75,7 +74,7 @@ export var layoutPlugin = function layoutPlugin(_ref) {
|
|
|
75
74
|
}
|
|
76
75
|
}]);
|
|
77
76
|
}
|
|
78
|
-
if (
|
|
77
|
+
if (expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
79
78
|
var _api$uiControlRegistr;
|
|
80
79
|
api === null || api === void 0 || (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 || _api$uiControlRegistr.actions.register(getLayoutColumnMenuComponents({
|
|
81
80
|
api: api
|
|
@@ -100,7 +99,7 @@ export var layoutPlugin = function layoutPlugin(_ref) {
|
|
|
100
99
|
return createLayoutPlugin(options, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
|
|
101
100
|
}
|
|
102
101
|
}];
|
|
103
|
-
if (
|
|
102
|
+
if (expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
104
103
|
plugins.push({
|
|
105
104
|
name: 'layoutKeymap',
|
|
106
105
|
plugin: function plugin() {
|
|
@@ -365,7 +364,7 @@ export var layoutPlugin = function layoutPlugin(_ref) {
|
|
|
365
364
|
popupsMountPoint = _ref5.popupsMountPoint,
|
|
366
365
|
popupsBoundariesElement = _ref5.popupsBoundariesElement,
|
|
367
366
|
popupsScrollableElement = _ref5.popupsScrollableElement;
|
|
368
|
-
return /*#__PURE__*/React.createElement(React.Fragment, null, editorExperiment('advanced_layouts', true) ? /*#__PURE__*/React.createElement(GlobalStylesWrapper, null) : null,
|
|
367
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, editorExperiment('advanced_layouts', true) ? /*#__PURE__*/React.createElement(GlobalStylesWrapper, null) : null, expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) && editorView ? /*#__PURE__*/React.createElement(LayoutColumnMenu, {
|
|
369
368
|
api: api,
|
|
370
369
|
editorView: editorView,
|
|
371
370
|
mountTo: popupsMountPoint,
|
|
@@ -9,7 +9,7 @@ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state
|
|
|
9
9
|
import { Mapping, StepMap } from '@atlaskit/editor-prosemirror/transform';
|
|
10
10
|
import { safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
11
11
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
12
|
-
import {
|
|
12
|
+
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
13
13
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
14
14
|
import { DEFAULT_LAYOUT_COLUMN_VALIGN, EVEN_DISTRIBUTED_COL_WIDTHS, MAX_LAYOUT_COLUMNS, MAX_STANDARD_LAYOUT_COLUMNS, MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
|
|
15
15
|
import { pluginKey } from './plugin-key';
|
|
@@ -586,7 +586,7 @@ var insertLayoutColumnAt = function insertLayoutColumnAt(side, editorAnalyticsAP
|
|
|
586
586
|
var inputMethod = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : INPUT_METHOD.LAYOUT_COLUMN_MENU;
|
|
587
587
|
return function (_ref5) {
|
|
588
588
|
var tr = _ref5.tr;
|
|
589
|
-
if (!
|
|
589
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
590
590
|
return null;
|
|
591
591
|
}
|
|
592
592
|
var selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
|
|
@@ -682,7 +682,7 @@ export var setLayoutColumnValign = function setLayoutColumnValign(_ref8, editorA
|
|
|
682
682
|
inputMethod = _ref8$inputMethod === void 0 ? INPUT_METHOD.LAYOUT_COLUMN_MENU : _ref8$inputMethod;
|
|
683
683
|
return function (_ref9) {
|
|
684
684
|
var tr = _ref9.tr;
|
|
685
|
-
if (!
|
|
685
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
686
686
|
return null;
|
|
687
687
|
}
|
|
688
688
|
var selectedLayoutColumnsResult = getSelectedLayoutColumnsFromSelection(tr.selection);
|
|
@@ -739,7 +739,7 @@ export var distributeLayoutColumns = function distributeLayoutColumns(editorAnal
|
|
|
739
739
|
target = _ref10$target === void 0 ? 'selectedColumns' : _ref10$target;
|
|
740
740
|
return function (_ref11) {
|
|
741
741
|
var tr = _ref11.tr;
|
|
742
|
-
if (!
|
|
742
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
743
743
|
return null;
|
|
744
744
|
}
|
|
745
745
|
var selectedLayoutColumnsResult = target === 'allColumns' ? getAllLayoutColumnsFromSelection(tr.selection) : getSelectedLayoutColumnsFromSelection(tr.selection);
|
|
@@ -850,7 +850,7 @@ export var deleteLayoutColumn = function deleteLayoutColumn() {
|
|
|
850
850
|
return function (_ref18) {
|
|
851
851
|
var _api$blockControls4;
|
|
852
852
|
var tr = _ref18.tr;
|
|
853
|
-
if (!
|
|
853
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
|
|
854
854
|
return null;
|
|
855
855
|
}
|
|
856
856
|
var selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
|
|
@@ -4,22 +4,50 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
|
|
|
4
4
|
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; }
|
|
5
5
|
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
6
6
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
7
|
-
import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
|
|
7
|
+
import { createSelectionClickHandler, GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
|
|
8
8
|
import { filterCommand as filter } from '@atlaskit/editor-common/utils';
|
|
9
9
|
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
10
10
|
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
11
11
|
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
12
12
|
import { findParentNodeClosestToPos, findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
13
13
|
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
14
|
+
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
14
15
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
15
16
|
import { fixColumnSizes, fixColumnStructure, getSelectedLayout, LAYOUT_COLUMN_INSERT_META } from './actions';
|
|
16
17
|
import { getColumnDividerDecorations } from './column-resize-divider';
|
|
17
18
|
import { EVEN_DISTRIBUTED_COL_WIDTHS } from './consts';
|
|
18
19
|
import { pluginKey } from './plugin-key';
|
|
19
20
|
import { pluginKey as layoutResizingPluginKey } from './resizing';
|
|
20
|
-
import { getMaybeLayoutSection } from './utils';
|
|
21
|
+
import { getGapCursorTargetForBlankSpaceClick, getMaybeLayoutSection, isParagraphBlankSpaceTarget } from './utils';
|
|
21
22
|
import { getSelectedLayoutColumnsFromSelection } from './utils/layout-column-selection';
|
|
22
23
|
export var DEFAULT_LAYOUT = 'two_equal';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared blank-space gap cursor placement, used by both `handleClick` and `handleClickOn`
|
|
27
|
+
* (the latter catches clicks on atomic node views that stop propagation before `handleClick`).
|
|
28
|
+
* Returns `true` when it placed a selection and consumed the click, else `false`.
|
|
29
|
+
*/
|
|
30
|
+
var applyBlankSpaceGapCursor = function applyBlankSpaceGapCursor(view, event) {
|
|
31
|
+
if (!expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) || !editorExperiment('advanced_layouts', true)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
var gapTarget = getGapCursorTargetForBlankSpaceClick(view, event);
|
|
35
|
+
if (gapTarget === undefined) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
var $pos = view.state.doc.resolve(gapTarget.pos);
|
|
39
|
+
// A paragraph child takes a TextSelection (caret at the edge) rather than a gap cursor.
|
|
40
|
+
var isParagraphTarget = isParagraphBlankSpaceTarget(view, gapTarget);
|
|
41
|
+
var nextSelection = isParagraphTarget ? TextSelection.near($pos, gapTarget.side === 'left' ? 1 : -1) : new GapCursorSelection($pos, gapTarget.side === 'left' ? Side.LEFT : Side.RIGHT);
|
|
42
|
+
// Idempotency guard: `mousedown` already placed this selection, but the browser still
|
|
43
|
+
// fires `mouseup`, so `handleClick`/`handleClickOn` re-run for the same click. Consume it
|
|
44
|
+
// without re-dispatching (which would add a redundant undo entry).
|
|
45
|
+
if (view.state.selection.eq(nextSelection)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
view.dispatch(view.state.tr.setSelection(nextSelection).scrollIntoView());
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
23
51
|
var isWholeSelectionInsideLayoutColumn = function isWholeSelectionInsideLayoutColumn(state) {
|
|
24
52
|
// Since findParentNodeOfType doesn't check if selection.to shares the parent, we do this check ourselves
|
|
25
53
|
var fromParent = findParentNodeOfType(state.schema.nodes.layoutColumn)(state.selection);
|
|
@@ -265,14 +293,52 @@ export default (function (options, editorAnalyticsAPI) {
|
|
|
265
293
|
Backspace: handleDeleteLayoutColumn,
|
|
266
294
|
Delete: handleDeleteLayoutColumn
|
|
267
295
|
}),
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
handleDOMEvents: {
|
|
297
|
+
// Place the gap cursor on `mousedown` (not `mouseup`) so the caret never flashes
|
|
298
|
+
// inside a nested editable child first.
|
|
299
|
+
mousedown: function mousedown(view, event) {
|
|
300
|
+
var target = event.target;
|
|
301
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
if (applyBlankSpaceGapCursor(view, event)) {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
// `preventDefault()` blocks the editor focus that makes the gap cursor blink,
|
|
307
|
+
// so restore it here. The `handleClick`/`handleClickOn` paths don't need this.
|
|
308
|
+
if (!view.hasFocus()) {
|
|
309
|
+
view.focus();
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
handleClickOn: function () {
|
|
317
|
+
var selectionClickHandler = createSelectionClickHandler(['layoutColumn'], function (target) {
|
|
318
|
+
return target.hasAttribute('data-layout-section') || target.hasAttribute('data-layout-column');
|
|
319
|
+
}, {
|
|
320
|
+
useLongPressSelection: options.useLongPressSelection || false,
|
|
321
|
+
getNodeSelectionPos: function getNodeSelectionPos(state, nodePos) {
|
|
322
|
+
return state.doc.resolve(nodePos).before();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return function (view, pos, node, nodePos, event, direct) {
|
|
326
|
+
// Fallback for clicks on an atomic node view that the mousedown hook missed.
|
|
327
|
+
var target = event.target;
|
|
328
|
+
if (!(target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) && applyBlankSpaceGapCursor(view, event)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
return selectionClickHandler(view, pos, node, nodePos, event, direct);
|
|
332
|
+
};
|
|
333
|
+
}(),
|
|
334
|
+
handleClick: function handleClick(view, _pos, event) {
|
|
335
|
+
// Fallback for clicks the mousedown interceptor missed.
|
|
336
|
+
var target = event.target;
|
|
337
|
+
if (target !== null && target !== void 0 && target.hasAttribute('data-layout-section')) {
|
|
338
|
+
return false;
|
|
274
339
|
}
|
|
275
|
-
|
|
340
|
+
return applyBlankSpaceGapCursor(view, event);
|
|
341
|
+
}
|
|
276
342
|
},
|
|
277
343
|
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
|
|
278
344
|
var changes = [];
|
|
@@ -2,6 +2,7 @@ import { GapCursorSelection } from '@atlaskit/editor-common/selection';
|
|
|
2
2
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
3
3
|
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
4
|
import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
5
6
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
6
7
|
export var getMaybeLayoutSection = function getMaybeLayoutSection(state) {
|
|
7
8
|
var _state$schema$nodes = state.schema.nodes,
|
|
@@ -49,4 +50,195 @@ export var selectIntoLayout = function selectIntoLayout(view, posOfLayout) {
|
|
|
49
50
|
}
|
|
50
51
|
return tr;
|
|
51
52
|
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* For a blank-space click inside a layout column — above the first child (middle/bottom-aligned
|
|
56
|
+
* columns) or below the last child (any alignment) — return the ProseMirror position and side
|
|
57
|
+
* for a gap cursor. Returns `undefined` when the kill switch is ON, the click is outside a
|
|
58
|
+
* layoutColumn, or the Y coordinate is not in blank space.
|
|
59
|
+
*
|
|
60
|
+
* The `advanced_layouts` / `platform_editor_layout_column_menu` gates live in the caller
|
|
61
|
+
* (`applyBlankSpaceGapCursor`); only the kill switch is checked here.
|
|
62
|
+
*/
|
|
63
|
+
export var getGapCursorTargetForBlankSpaceClick = function getGapCursorTargetForBlankSpaceClick(view, event) {
|
|
64
|
+
var _columnNode$attrs;
|
|
65
|
+
if (fg('platform_editor_layout_column_menu_kill_switch_1')) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve the column from the DOM target so it works even when posAtCoords returns null
|
|
70
|
+
// (nothing rendered at the clicked Y).
|
|
71
|
+
var target = event.target;
|
|
72
|
+
var columnEl = target === null || target === void 0 ? void 0 : target.closest('[data-layout-column]');
|
|
73
|
+
if (!columnEl) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
var columnStartPos;
|
|
77
|
+
try {
|
|
78
|
+
columnStartPos = view.posAtDOM(columnEl, 0);
|
|
79
|
+
} catch (_unused) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// posAtDOM resolves at varying depths, so walk up to find the layoutColumn.
|
|
84
|
+
var $columnStart = view.state.doc.resolve(columnStartPos);
|
|
85
|
+
var depth = -1;
|
|
86
|
+
for (var d = $columnStart.depth; d >= 0; d--) {
|
|
87
|
+
if ($columnStart.node(d).type.name === 'layoutColumn') {
|
|
88
|
+
depth = d;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (depth < 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
var columnNode = $columnStart.node(depth);
|
|
96
|
+
if (columnNode.childCount === 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
var columnContentStart = $columnStart.start(depth);
|
|
100
|
+
var columnEndPos = $columnStart.end(depth);
|
|
101
|
+
var getChildDom = function getChildDom(nodePos) {
|
|
102
|
+
try {
|
|
103
|
+
var dom = view.nodeDOM(nodePos);
|
|
104
|
+
return dom instanceof Element ? dom : null;
|
|
105
|
+
} catch (_unused2) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var valign = (_columnNode$attrs = columnNode.attrs) === null || _columnNode$attrs === void 0 ? void 0 : _columnNode$attrs.valign;
|
|
110
|
+
var isNonTopAligned = valign && valign !== 'top';
|
|
111
|
+
|
|
112
|
+
// Use the column rect (not child rects) for above/below detection: it stays stable as
|
|
113
|
+
// gap-cursor widgets shift child DOM positions between repeated clicks.
|
|
114
|
+
var columnRect = columnEl.getBoundingClientRect();
|
|
115
|
+
|
|
116
|
+
// Click ABOVE the first child (only for middle/bottom-aligned columns).
|
|
117
|
+
var firstChildPos = columnContentStart;
|
|
118
|
+
var firstChildDom = getChildDom(firstChildPos);
|
|
119
|
+
if (isNonTopAligned && firstChildDom) {
|
|
120
|
+
var rect = firstChildDom.getBoundingClientRect();
|
|
121
|
+
if (event.clientY < rect.top && event.clientY >= columnRect.top) {
|
|
122
|
+
return {
|
|
123
|
+
pos: firstChildPos,
|
|
124
|
+
side: 'left'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Click BELOW the last child (for any column alignment).
|
|
130
|
+
var lastChild = columnNode.lastChild;
|
|
131
|
+
var lastChildEndPos = columnEndPos;
|
|
132
|
+
var lastChildStartPos = lastChild ? lastChildEndPos - lastChild.nodeSize : columnContentStart;
|
|
133
|
+
var lastChildDom = lastChild ? getChildDom(lastChildStartPos) : null;
|
|
134
|
+
if (lastChild && lastChildDom) {
|
|
135
|
+
var _rect = lastChildDom.getBoundingClientRect();
|
|
136
|
+
if (event.clientY > _rect.bottom && event.clientY <= columnRect.bottom) {
|
|
137
|
+
return {
|
|
138
|
+
pos: lastChildEndPos,
|
|
139
|
+
side: 'right'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: click lands ON a single atomic child that fills the column (mediaSingle/expand),
|
|
145
|
+
// so the above/below checks never fired.
|
|
146
|
+
if (columnNode.childCount === 1) {
|
|
147
|
+
var onlyChild = columnNode.firstChild;
|
|
148
|
+
// Exclude `panel`: its wrapper makes `view.nodeDOM` non-null and intercepts clicks, so the
|
|
149
|
+
// guard below would wrongly fire for in-panel blank-space clicks (which have their own
|
|
150
|
+
// native gap cursor).
|
|
151
|
+
if (onlyChild && onlyChild.type.name !== 'paragraph' && onlyChild.type.name !== 'panel') {
|
|
152
|
+
// Bail when the click is on the child's own content. For media the wrapper is full-width
|
|
153
|
+
// so test against the <img> rect; resolve it only for a direct mediaSingle child (else
|
|
154
|
+
// getContentRect could grab an image nested in an expand and break its toggle).
|
|
155
|
+
var contentRect = onlyChild.type.name === 'mediaSingle' ? getContentRect(firstChildDom) : null;
|
|
156
|
+
if (contentRect) {
|
|
157
|
+
var insideImage = event.clientX >= contentRect.left && event.clientX <= contentRect.right && event.clientY >= contentRect.top && event.clientY <= contentRect.bottom;
|
|
158
|
+
if (insideImage) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// Other atomics: bail when posAtCoords resolves strictly inside the node range.
|
|
163
|
+
var coordPos = null;
|
|
164
|
+
try {
|
|
165
|
+
coordPos = view.posAtCoords({
|
|
166
|
+
left: event.clientX,
|
|
167
|
+
top: event.clientY
|
|
168
|
+
});
|
|
169
|
+
} catch (_unused3) {
|
|
170
|
+
coordPos = null;
|
|
171
|
+
}
|
|
172
|
+
if (coordPos && coordPos.pos > firstChildPos && coordPos.pos < lastChildEndPos) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Fire when the child DOM is resolvable, or when it's null (media not yet loaded) but
|
|
178
|
+
// the click target is the column itself (no node view intercepted it).
|
|
179
|
+
var targetEl = event.target;
|
|
180
|
+
var targetIsColumn = targetEl === columnEl;
|
|
181
|
+
var shouldUseFallback = firstChildDom !== null || targetIsColumn;
|
|
182
|
+
if (shouldUseFallback) {
|
|
183
|
+
var side = getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, event.clientX, event.clientY);
|
|
184
|
+
return side === 'left' ? {
|
|
185
|
+
pos: firstChildPos,
|
|
186
|
+
side: 'left'
|
|
187
|
+
} : {
|
|
188
|
+
pos: lastChildEndPos,
|
|
189
|
+
side: 'right'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The tight `<img>` content rect (or `null`). The outer wrapper often fills the whole column
|
|
199
|
+
* width, so the `<img>` rect is needed to tell "beside the image" from "on the image".
|
|
200
|
+
*/
|
|
201
|
+
var getContentRect = function getContentRect(firstChildDom) {
|
|
202
|
+
var img = firstChildDom === null || firstChildDom === void 0 ? void 0 : firstChildDom.querySelector('img');
|
|
203
|
+
return img ? img.getBoundingClientRect() : null;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Which side of an atomic child a blank-space click belongs to. Prefers the tight content (image)
|
|
208
|
+
* rect when available — using its midpoint so it's direction-agnostic (handles RTL right-aligned
|
|
209
|
+
* images) — otherwise falls back to the column's vertical midpoint.
|
|
210
|
+
*/
|
|
211
|
+
var getGapCursorSideForBlankSpaceClick = function getGapCursorSideForBlankSpaceClick(firstChildDom, columnRect, clientX, clientY) {
|
|
212
|
+
var contentRect = getContentRect(firstChildDom);
|
|
213
|
+
if (contentRect) {
|
|
214
|
+
if (clientY < contentRect.top) {
|
|
215
|
+
return 'left';
|
|
216
|
+
}
|
|
217
|
+
if (clientY > contentRect.bottom) {
|
|
218
|
+
return 'right';
|
|
219
|
+
}
|
|
220
|
+
if (clientX < (contentRect.left + contentRect.right) / 2) {
|
|
221
|
+
return 'left';
|
|
222
|
+
}
|
|
223
|
+
return 'right';
|
|
224
|
+
}
|
|
225
|
+
var columnMidY = columnRect.top + columnRect.height / 2;
|
|
226
|
+
return clientY < columnMidY ? 'left' : 'right';
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* True when the blank-space click target child is a paragraph, so the caller uses a TextSelection
|
|
231
|
+
* instead of a gap cursor. LEFT inspects the first child, RIGHT the last child.
|
|
232
|
+
*/
|
|
233
|
+
export var isParagraphBlankSpaceTarget = function isParagraphBlankSpaceTarget(view, gapTarget) {
|
|
234
|
+
var pos = gapTarget.pos,
|
|
235
|
+
side = gapTarget.side;
|
|
236
|
+
var doc = view.state.doc;
|
|
237
|
+
try {
|
|
238
|
+
var $pos = doc.resolve(pos);
|
|
239
|
+
var childNode = side === 'left' ? $pos.nodeAfter : $pos.nodeBefore;
|
|
240
|
+
return (childNode === null || childNode === void 0 ? void 0 : childNode.type.name) === 'paragraph';
|
|
241
|
+
} catch (_unused4) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
52
244
|
};
|
|
@@ -11,3 +11,22 @@ export declare const getMaybeLayoutSection: (state: EditorState) => ContentNodeW
|
|
|
11
11
|
* @returns Transaction or undefined
|
|
12
12
|
*/
|
|
13
13
|
export declare const selectIntoLayout: (view: EditorView, posOfLayout: number, childIndex?: number) => Transaction | undefined;
|
|
14
|
+
export type GapCursorTarget = {
|
|
15
|
+
pos: number;
|
|
16
|
+
side: 'left' | 'right';
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* For a blank-space click inside a layout column — above the first child (middle/bottom-aligned
|
|
20
|
+
* columns) or below the last child (any alignment) — return the ProseMirror position and side
|
|
21
|
+
* for a gap cursor. Returns `undefined` when the kill switch is ON, the click is outside a
|
|
22
|
+
* layoutColumn, or the Y coordinate is not in blank space.
|
|
23
|
+
*
|
|
24
|
+
* The `advanced_layouts` / `platform_editor_layout_column_menu` gates live in the caller
|
|
25
|
+
* (`applyBlankSpaceGapCursor`); only the kill switch is checked here.
|
|
26
|
+
*/
|
|
27
|
+
export declare const getGapCursorTargetForBlankSpaceClick: (view: EditorView, event: MouseEvent) => GapCursorTarget | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* True when the blank-space click target child is a paragraph, so the caller uses a TextSelection
|
|
30
|
+
* instead of a gap cursor. LEFT inspects the first child, RIGHT the last child.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isParagraphBlankSpaceTarget: (view: EditorView, gapTarget: GapCursorTarget) => boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-layout",
|
|
3
|
-
"version": "13.2.
|
|
3
|
+
"version": "13.2.3",
|
|
4
4
|
"description": "Layout plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"bind-event-listener": "^3.0.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@atlaskit/editor-common": "^116.
|
|
52
|
+
"@atlaskit/editor-common": "^116.13.0",
|
|
53
53
|
"react": "^18.2.0",
|
|
54
54
|
"react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
|
|
55
55
|
},
|