@atlaskit/editor-plugin-layout 8.0.22 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/editor-plugin-layout
2
2
 
3
+ ## 8.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`fb96753c1753e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/fb96753c1753e) -
8
+ [ux] Introduce adjustable layout column drag handle and update logic behind
9
+ platform_editor_layout_column_resize_handle experiment
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+
3
15
  ## 8.0.22
4
16
 
5
17
  ### Patch Changes
@@ -330,6 +330,11 @@ function forceColumnWidths(state, tr, pos, presetLayout) {
330
330
  }
331
331
  return tr.replaceWith(pos + 1, pos + node.nodeSize - 1, columnWidth(node, state.schema, getWidthsForPreset(presetLayout)));
332
332
  }
333
+
334
+ /**
335
+ * Forces a layout section node to match the given preset layout by adjusting
336
+ * its column structure and widths, then restoring the original selection.
337
+ */
333
338
  function forceSectionToPresetLayout(state, node, pos, presetLayout) {
334
339
  var forceColumnStructureFn = (0, _experiments.editorExperiment)('advanced_layouts', true) ? forceColumnStructureNew : forceColumnStructure;
335
340
  var tr = forceColumnStructureFn(state, node, pos, presetLayout);
@@ -381,6 +386,13 @@ var setPresetLayout = exports.setPresetLayout = function setPresetLayout(editorA
381
386
  };
382
387
  function layoutNeedChanges(node) {
383
388
  if ((0, _experiments.editorExperiment)('advanced_layouts', true)) {
389
+ if ((0, _experiments.editorExperiment)('platform_editor_layout_column_resize_handle', true)) {
390
+ // Custom widths that sum to 100% are valid and should not be forced back to presets
391
+ if (isValidLayoutWidthDistributions(node)) {
392
+ return false;
393
+ }
394
+ return true;
395
+ }
384
396
  return !getPresetLayout(node) || !isValidLayoutWidthDistributions(node);
385
397
  }
386
398
  return !getPresetLayout(node);
@@ -461,6 +473,11 @@ var fixColumnStructure = exports.fixColumnStructure = function fixColumnStructur
461
473
  var node = state.doc.nodeAt(pos);
462
474
  if (node) {
463
475
  if (node.childCount !== getWidthsForPreset(selectedLayout).length) {
476
+ // If the resize handle experiment is on and widths are valid, don't force preset
477
+ // (column count mismatch might be from a different preset being selected)
478
+ if ((0, _experiments.editorExperiment)('advanced_layouts', true) && (0, _experiments.editorExperiment)('platform_editor_layout_column_resize_handle', true) && isValidLayoutWidthDistributions(node)) {
479
+ return;
480
+ }
464
481
  return forceSectionToPresetLayout(state, node, pos, selectedLayout);
465
482
  }
466
483
  if (!isValidLayoutWidthDistributions(node) && (0, _experiments.editorExperiment)('advanced_layouts', true)) {
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.getColumnDividerDecorations = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _bindEventListener = require("bind-event-listener");
10
+ var _model = require("@atlaskit/editor-prosemirror/model");
11
+ var _view = require("@atlaskit/editor-prosemirror/view");
12
+ 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; }
13
+ 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; }
14
+ // Class names for the column resize divider widget — must stay in sync with layout.ts in editor-core
15
+ var layoutColumnDividerClassName = 'layout-column-divider';
16
+ var layoutColumnDividerRailClassName = 'layout-column-divider-rail';
17
+ var layoutColumnDividerThumbClassName = 'layout-column-divider-thumb';
18
+
19
+ // Minimum column width percentage to prevent columns from collapsing
20
+ var MIN_COLUMN_WIDTH_PERCENT = 5;
21
+
22
+ // Module-level drag state so it survives widget DOM recreation during transactions.
23
+ var dragState = null;
24
+
25
+ /**
26
+ * Dispatches a single undoable ProseMirror transaction to commit the final
27
+ * column widths after a drag completes.
28
+ */
29
+ var dispatchColumnWidths = function dispatchColumnWidths(view, sectionPos, leftColIndex, leftWidth, rightWidth) {
30
+ var state = view.state;
31
+ var sectionNode = state.doc.nodeAt(sectionPos);
32
+ if (!sectionNode) {
33
+ return;
34
+ }
35
+ var layoutColumn = state.schema.nodes.layoutColumn;
36
+ var tr = state.tr;
37
+ var newColumns = [];
38
+ sectionNode.forEach(function (child, _offset, index) {
39
+ if (child.type === layoutColumn) {
40
+ var newWidth = child.attrs.width;
41
+ if (index === leftColIndex) {
42
+ newWidth = Number(leftWidth.toFixed(2));
43
+ } else if (index === leftColIndex + 1) {
44
+ newWidth = Number(rightWidth.toFixed(2));
45
+ }
46
+ newColumns.push(layoutColumn.create(_objectSpread(_objectSpread({}, child.attrs), {}, {
47
+ width: newWidth
48
+ }), child.content, child.marks));
49
+ } else {
50
+ newColumns.push(child);
51
+ }
52
+ });
53
+ tr.replaceWith(sectionPos + 1, sectionPos + sectionNode.nodeSize - 1, _model.Fragment.from(newColumns));
54
+ tr.setMeta('layoutColumnResize', true);
55
+ tr.setMeta('scrollIntoView', false);
56
+ view.dispatch(tr);
57
+ };
58
+
59
+ /**
60
+ * Calculates new column widths from the current mouse X position during drag.
61
+ * Uses the columns-only width cached at mousedown — no layout reflow.
62
+ *
63
+ * The denominator is `columnsWidth` (the total flex container width minus
64
+ * divider widgets and flex gaps) so that a 1 px mouse movement corresponds
65
+ * to the exact same visual shift in the column boundary, eliminating the
66
+ * few-pixel drift that occurred when using the full section width.
67
+ */
68
+ var calcDragWidths = function calcDragWidths(clientX) {
69
+ if (!dragState) {
70
+ return null;
71
+ }
72
+ var _dragState = dragState,
73
+ columnsWidth = _dragState.columnsWidth;
74
+ if (columnsWidth === 0) {
75
+ return null;
76
+ }
77
+ var deltaX = clientX - dragState.startX;
78
+ var combinedWidth = dragState.startLeftWidth + dragState.startRightWidth;
79
+ var deltaPercent = deltaX / columnsWidth * 100;
80
+ var leftWidth = dragState.startLeftWidth + deltaPercent;
81
+ var rightWidth = dragState.startRightWidth - deltaPercent;
82
+ if (leftWidth < MIN_COLUMN_WIDTH_PERCENT) {
83
+ leftWidth = MIN_COLUMN_WIDTH_PERCENT;
84
+ rightWidth = combinedWidth - MIN_COLUMN_WIDTH_PERCENT;
85
+ } else if (rightWidth < MIN_COLUMN_WIDTH_PERCENT) {
86
+ rightWidth = MIN_COLUMN_WIDTH_PERCENT;
87
+ leftWidth = combinedWidth - MIN_COLUMN_WIDTH_PERCENT;
88
+ }
89
+ return {
90
+ leftWidth: leftWidth,
91
+ rightWidth: rightWidth
92
+ };
93
+ };
94
+ var onDragMouseMove = function onDragMouseMove(e) {
95
+ if (!dragState) {
96
+ return;
97
+ }
98
+
99
+ // If the mouse button was released outside the window (e.g. over browser chrome
100
+ // or an iframe), we won't receive a mouseup on ownerDoc. Detect this by checking
101
+ // whether any button is still held — if not, treat it as a drag end.
102
+ if (e.buttons === 0) {
103
+ onDragEnd(e.clientX);
104
+ return;
105
+ }
106
+
107
+ // Always capture the latest clientX so the rAF callback uses the most recent
108
+ // mouse position. Previously, intermediate positions were dropped when an rAF
109
+ // was already scheduled, causing the column boundary to lag behind the cursor.
110
+ dragState.lastClientX = e.clientX;
111
+
112
+ // If a paint frame is already scheduled it will pick up lastClientX — no need
113
+ // to schedule another one.
114
+ if (dragState.rafId !== null) {
115
+ return;
116
+ }
117
+ dragState.rafId = requestAnimationFrame(function () {
118
+ if (!dragState) {
119
+ return;
120
+ }
121
+ dragState.rafId = null;
122
+ var widths = calcDragWidths(dragState.lastClientX);
123
+ if (!widths) {
124
+ return;
125
+ }
126
+
127
+ // Write flex-basis directly onto the column elements' inline styles for immediate
128
+ // visual feedback. This beats PM's own inline flex-basis value without dispatching
129
+ // any PM transaction — keeping drag completely off the ProseMirror render path.
130
+ // The LayoutColumnView.ignoreMutation implementation ensures PM's MutationObserver
131
+ // does not revert these style changes mid-drag.
132
+ dragState.hasDragged = true;
133
+ dragState.leftColEl.style.flexBasis = "".concat(widths.leftWidth, "%");
134
+ dragState.rightColEl.style.flexBasis = "".concat(widths.rightWidth, "%");
135
+ });
136
+ };
137
+
138
+ /**
139
+ * Shared teardown for all drag-end paths (mouseup, missed mouseup detected via
140
+ * e.buttons===0 on mousemove, window blur, and visibilitychange). Commits the
141
+ * final column widths if a real drag occurred.
142
+ */
143
+ var onDragEnd = function onDragEnd(clientX) {
144
+ if (!dragState) {
145
+ return;
146
+ }
147
+ var _dragState2 = dragState,
148
+ view = _dragState2.view,
149
+ sectionPos = _dragState2.sectionPos,
150
+ leftColIndex = _dragState2.leftColIndex,
151
+ leftColEl = _dragState2.leftColEl,
152
+ rightColEl = _dragState2.rightColEl,
153
+ hasDragged = _dragState2.hasDragged,
154
+ rafId = _dragState2.rafId,
155
+ startLeftWidth = _dragState2.startLeftWidth,
156
+ startRightWidth = _dragState2.startRightWidth,
157
+ unbindListeners = _dragState2.unbindListeners;
158
+ unbindListeners();
159
+
160
+ // Cancel any pending rAF so a stale frame doesn't write styles after teardown.
161
+ if (rafId !== null) {
162
+ cancelAnimationFrame(rafId);
163
+ }
164
+ var ownerDoc = view.dom.ownerDocument;
165
+ ownerDoc.body.style.userSelect = '';
166
+ ownerDoc.body.style.cursor = '';
167
+ var widths = calcDragWidths(clientX);
168
+ dragState = null;
169
+ if (!hasDragged) {
170
+ // The user clicked without dragging — no flex-basis overrides were written,
171
+ // so there is nothing to clean up and no transaction to dispatch.
172
+ return;
173
+ }
174
+
175
+ // Clear the drag-time flex-basis overrides. The PM transaction below will
176
+ // write the committed widths back into the node attrs and re-render the DOM.
177
+ leftColEl.style.flexBasis = '';
178
+ rightColEl.style.flexBasis = '';
179
+ if (widths && (widths.leftWidth !== startLeftWidth || widths.rightWidth !== startRightWidth)) {
180
+ dispatchColumnWidths(view, sectionPos, leftColIndex, widths.leftWidth, widths.rightWidth);
181
+ }
182
+ };
183
+ var onDragMouseUp = function onDragMouseUp(e) {
184
+ onDragEnd(e.clientX);
185
+ };
186
+
187
+ /**
188
+ * Called when the window loses focus (blur) or the tab becomes hidden
189
+ * (visibilitychange). In either case the user can't be dragging anymore,
190
+ * so we commit the drag at the last known mouse position.
191
+ */
192
+ var onDragCancel = function onDragCancel() {
193
+ if (!dragState) {
194
+ return;
195
+ }
196
+ // Commit at the last captured mouse position rather than startX, so the
197
+ // columns stay where the user last saw them.
198
+ onDragEnd(dragState.lastClientX);
199
+ };
200
+
201
+ /**
202
+ * Creates a column divider widget DOM element with drag-to-resize interaction for
203
+ * the adjacent layout columns. During drag, flex-basis is mutated directly on the
204
+ * column DOM elements for zero-overhead visual feedback (no PM transactions).
205
+ * A single undoable PM transaction is dispatched on mouseup to commit the final widths.
206
+ */
207
+ var createColumnDividerWidget = function createColumnDividerWidget(view, sectionPos, columnIndex // index of the column to the RIGHT of this divider
208
+ ) {
209
+ var ownerDoc = view.dom.ownerDocument;
210
+
211
+ // Outer container: wide transparent hit area for easy grabbing, zero flex footprint
212
+ var divider = ownerDoc.createElement('div');
213
+ divider.classList.add(layoutColumnDividerClassName);
214
+ divider.contentEditable = 'false';
215
+
216
+ // Rail: styled via layoutColumnDividerStyles in layout.ts
217
+ var rail = ownerDoc.createElement('div');
218
+ rail.classList.add(layoutColumnDividerRailClassName);
219
+ divider.appendChild(rail);
220
+
221
+ // Thumb: styled via layoutColumnDividerStyles in layout.ts
222
+ var thumb = ownerDoc.createElement('div');
223
+ thumb.classList.add(layoutColumnDividerThumbClassName);
224
+ rail.appendChild(thumb);
225
+ var leftColIndex = columnIndex - 1;
226
+ (0, _bindEventListener.bind)(divider, {
227
+ type: 'mousedown',
228
+ listener: function listener(e) {
229
+ var _ownerDoc$defaultView;
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ var sectionNode = view.state.doc.nodeAt(sectionPos);
233
+ if (!sectionNode) {
234
+ return;
235
+ }
236
+
237
+ // Get the initial widths of the two adjacent columns
238
+ var leftCol = null;
239
+ var rightCol = null;
240
+ sectionNode.forEach(function (child, _offset, index) {
241
+ if (index === leftColIndex) {
242
+ leftCol = child;
243
+ }
244
+ if (index === leftColIndex + 1) {
245
+ rightCol = child;
246
+ }
247
+ });
248
+ if (!leftCol || !rightCol) {
249
+ return;
250
+ }
251
+ var sectionElement = divider.closest('[data-layout-section]');
252
+ if (!(sectionElement instanceof HTMLElement)) {
253
+ return;
254
+ }
255
+
256
+ // Capture the two adjacent column DOM elements upfront so mousemove can
257
+ // mutate their flex-basis directly without any PM transaction overhead.
258
+ // Query by data-layout-column-index (stamped by LayoutColumnView) rather than
259
+ // relying on positional order of [data-layout-column] elements, which would
260
+ // break if the DOM structure or ordering ever changes.
261
+ var leftColEl = sectionElement.querySelector("[data-layout-column-index=\"".concat(leftColIndex, "\"]"));
262
+ var rightColEl = sectionElement.querySelector("[data-layout-column-index=\"".concat(leftColIndex + 1, "\"]"));
263
+ if (!leftColEl || !rightColEl) {
264
+ return;
265
+ }
266
+ var unbindMove = (0, _bindEventListener.bind)(ownerDoc, {
267
+ type: 'mousemove',
268
+ listener: onDragMouseMove
269
+ });
270
+ var unbindUp = (0, _bindEventListener.bind)(ownerDoc, {
271
+ type: 'mouseup',
272
+ listener: onDragMouseUp
273
+ });
274
+ // If the user releases the mouse outside the browser window (e.g. over the
275
+ // OS desktop) and then brings the cursor back, we won't get a mouseup on
276
+ // ownerDoc. Listening on window for blur and on the document for
277
+ // visibilitychange catches tab switches and window focus loss respectively.
278
+ var unbindBlur = (0, _bindEventListener.bind)((_ownerDoc$defaultView = ownerDoc.defaultView) !== null && _ownerDoc$defaultView !== void 0 ? _ownerDoc$defaultView : window, {
279
+ type: 'blur',
280
+ listener: onDragCancel
281
+ });
282
+ var unbindVisibility = (0, _bindEventListener.bind)(ownerDoc, {
283
+ type: 'visibilitychange',
284
+ listener: onDragCancel
285
+ });
286
+
287
+ // Compute the width available to columns only (excluding divider widgets and
288
+ // flex gaps). Using this as the denominator ensures that a 1 px mouse delta
289
+ // translates to the exact pixel shift on the column boundary.
290
+ var sectionRect = sectionElement.getBoundingClientRect();
291
+ var dividers = sectionElement.querySelectorAll(".".concat(layoutColumnDividerClassName));
292
+ var dividersWidth = 0;
293
+ dividers.forEach(function (d) {
294
+ dividersWidth += d.getBoundingClientRect().width;
295
+ });
296
+ // Account for CSS gap between flex children. The gap is applied between
297
+ // every pair of direct children (columns + divider widgets).
298
+ var computedGap = parseFloat(getComputedStyle(sectionElement).gap || '0');
299
+ var childCount = sectionElement.children.length;
300
+ var totalGap = childCount > 1 ? computedGap * (childCount - 1) : 0;
301
+ var columnsWidth = sectionRect.width - dividersWidth - totalGap;
302
+ dragState = {
303
+ hasDragged: false,
304
+ lastClientX: e.clientX,
305
+ rafId: null,
306
+ view: view,
307
+ sectionPos: sectionPos,
308
+ leftColIndex: leftColIndex,
309
+ leftColEl: leftColEl,
310
+ rightColEl: rightColEl,
311
+ startX: e.clientX,
312
+ startLeftWidth: leftCol.attrs.width,
313
+ startRightWidth: rightCol.attrs.width,
314
+ columnsWidth: columnsWidth,
315
+ sectionElement: sectionElement,
316
+ unbindListeners: function unbindListeners() {
317
+ unbindMove();
318
+ unbindUp();
319
+ unbindBlur();
320
+ unbindVisibility();
321
+ }
322
+ };
323
+ ownerDoc.body.style.userSelect = 'none';
324
+ ownerDoc.body.style.cursor = 'col-resize';
325
+ }
326
+ });
327
+ return divider;
328
+ };
329
+
330
+ /**
331
+ * Returns ProseMirror Decoration widgets for column dividers between layout columns.
332
+ * Each divider supports drag-to-resize interaction for the adjacent columns.
333
+ */
334
+ var getColumnDividerDecorations = exports.getColumnDividerDecorations = function getColumnDividerDecorations(state, view) {
335
+ var decorations = [];
336
+ if (!view) {
337
+ return decorations;
338
+ }
339
+ var layoutSection = state.schema.nodes.layoutSection;
340
+ state.doc.descendants(function (node, pos) {
341
+ if (node.type === layoutSection) {
342
+ // Walk through layout column children and add dividers between them
343
+ node.forEach(function (child, offset, index) {
344
+ // Add a divider widget BEFORE every column except the first
345
+ if (index > 0) {
346
+ var sectionPos = pos;
347
+ var colIndex = index;
348
+ var widgetPos = pos + offset + 1; // position at the start of this column
349
+ decorations.push(_view.Decoration.widget(widgetPos, function () {
350
+ return createColumnDividerWidget(view, sectionPos, colIndex);
351
+ }, {
352
+ side: -1,
353
+ // place before the position
354
+ key: "layout-col-divider-".concat(pos, "-").concat(index),
355
+ ignoreSelection: true
356
+ }));
357
+ }
358
+ });
359
+ return false; // don't descend into children
360
+ }
361
+ return true; // continue descending
362
+ });
363
+ return decorations;
364
+ };
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = exports.DEFAULT_LAYOUT = void 0;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
8
9
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
10
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
10
11
  var _selection = require("@atlaskit/editor-common/selection");
@@ -13,9 +14,10 @@ var _keymap = require("@atlaskit/editor-prosemirror/keymap");
13
14
  var _model = require("@atlaskit/editor-prosemirror/model");
14
15
  var _state = require("@atlaskit/editor-prosemirror/state");
15
16
  var _utils2 = require("@atlaskit/editor-prosemirror/utils");
16
- var _view = require("@atlaskit/editor-prosemirror/view");
17
+ var _view2 = require("@atlaskit/editor-prosemirror/view");
17
18
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
18
19
  var _actions = require("./actions");
20
+ var _columnResizeDivider = require("./column-resize-divider");
19
21
  var _consts = require("./consts");
20
22
  var _pluginKey = require("./plugin-key");
21
23
  var _utils3 = require("./utils");
@@ -52,7 +54,7 @@ var moveCursorToNextColumn = function moveCursorToNextColumn(state, dispatch) {
52
54
  return true;
53
55
  };
54
56
  var getNodeDecoration = function getNodeDecoration(pos, node) {
55
- return [_view.Decoration.node(pos, pos + node.nodeSize, {
57
+ return [_view2.Decoration.node(pos, pos + node.nodeSize, {
56
58
  class: 'selected'
57
59
  })];
58
60
  };
@@ -98,8 +100,21 @@ var handleDeleteLayoutColumn = function handleDeleteLayoutColumn(state, dispatch
98
100
  return false;
99
101
  };
100
102
  var _default = exports.default = function _default(options) {
103
+ // Store a reference to the EditorView so widget decorations can dispatch transactions
104
+ var editorViewRef;
101
105
  return new _safePlugin.SafePlugin({
102
106
  key: _pluginKey.pluginKey,
107
+ view: function view(_view) {
108
+ editorViewRef = _view;
109
+ return {
110
+ update: function update(updatedView) {
111
+ editorViewRef = updatedView;
112
+ },
113
+ destroy: function destroy() {
114
+ editorViewRef = undefined;
115
+ }
116
+ };
117
+ },
103
118
  state: {
104
119
  init: function init(_, state) {
105
120
  return getInitialPluginState(options, state);
@@ -127,8 +142,17 @@ var _default = exports.default = function _default(options) {
127
142
  props: {
128
143
  decorations: function decorations(state) {
129
144
  var layoutState = _pluginKey.pluginKey.getState(state);
145
+ if ((0, _experiments.editorExperiment)('advanced_layouts', true) && (0, _experiments.editorExperiment)('platform_editor_layout_column_resize_handle', true)) {
146
+ var dividerDecorations = (0, _columnResizeDivider.getColumnDividerDecorations)(state, editorViewRef);
147
+ var selectedDecorations = layoutState.pos !== null ? getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)) : [];
148
+ var allDecorations = [].concat((0, _toConsumableArray2.default)(selectedDecorations), (0, _toConsumableArray2.default)(dividerDecorations));
149
+ if (allDecorations.length > 0) {
150
+ return _view2.DecorationSet.create(state.doc, allDecorations);
151
+ }
152
+ return undefined;
153
+ }
130
154
  if (layoutState.pos !== null) {
131
- return _view.DecorationSet.create(state.doc, getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)));
155
+ return _view2.DecorationSet.create(state.doc, getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)));
132
156
  }
133
157
  return undefined;
134
158
  },
@@ -164,6 +188,11 @@ var _default = exports.default = function _default(options) {
164
188
  if (!prevTr.docChanged) {
165
189
  return;
166
190
  }
191
+
192
+ // Skip fixing column sizes for column resize drag transactions
193
+ if ((0, _experiments.editorExperiment)('platform_editor_layout_column_resize_handle', true) && prevTr.getMeta('layoutColumnResize')) {
194
+ return;
195
+ }
167
196
  var change = (0, _actions.fixColumnSizes)(prevTr, newState);
168
197
  if (change) {
169
198
  changes.push(change);
@@ -1,18 +1,81 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
7
  exports.pluginKey = exports.default = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
10
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
7
11
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
12
+ var _model = require("@atlaskit/editor-prosemirror/model");
8
13
  var _state = require("@atlaskit/editor-prosemirror/state");
14
+ var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
9
15
  var _nodeviews = require("../nodeviews");
16
+ 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; }
17
+ 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; }
10
18
  var pluginKey = exports.pluginKey = new _state.PluginKey('layoutResizingPlugin');
19
+
20
+ /**
21
+ * Minimal node view for layoutColumn that delegates all DOM serialization to the
22
+ * NodeSpec's own toDOM, but overrides ignoreMutation to suppress style attribute
23
+ * mutations from ProseMirror's MutationObserver.
24
+ *
25
+ * This is necessary so that direct inline style mutations during column drag
26
+ * (e.g. setting flex-basis to give real-time visual feedback without dispatching
27
+ * PM transactions) are not "corrected" back by ProseMirror's DOM reconciliation.
28
+ */
29
+ var LayoutColumnView = /*#__PURE__*/function () {
30
+ function LayoutColumnView(node, view, getPos) {
31
+ (0, _classCallCheck2.default)(this, LayoutColumnView);
32
+ // Use the NodeSpec's own toDOM to produce the correct DOM structure and attributes.
33
+ var nodeType = view.state.schema.nodes[node.type.name];
34
+
35
+ // Fallback: create a plain div so PM always has a valid DOM node to work with.
36
+ // This path should never be reached in practice — layoutColumn always has a toDOM.
37
+ if (!nodeType.spec.toDOM) {
38
+ var fallbackDiv = document.createElement('div');
39
+ this.dom = fallbackDiv;
40
+ this.contentDOM = fallbackDiv;
41
+ return;
42
+ }
43
+ var _DOMSerializer$render = _model.DOMSerializer.renderSpec(document, nodeType.spec.toDOM(node)),
44
+ dom = _DOMSerializer$render.dom,
45
+ contentDOM = _DOMSerializer$render.contentDOM;
46
+ if (!(dom instanceof HTMLElement) || !(contentDOM instanceof HTMLElement)) {
47
+ var _fallbackDiv = document.createElement('div');
48
+ this.dom = _fallbackDiv;
49
+ this.contentDOM = _fallbackDiv;
50
+ return;
51
+ }
52
+ this.dom = dom;
53
+ this.contentDOM = contentDOM;
54
+
55
+ // Stamp the column's index within its parent section onto the DOM element so that
56
+ // column-resize-divider can query columns by index rather than relying on positional
57
+ // order of [data-layout-column] elements (which could break if the DOM structure changes).
58
+ var pos = getPos();
59
+ if (pos !== undefined) {
60
+ var $pos = view.state.doc.resolve(pos);
61
+ this.dom.setAttribute('data-layout-column-index', String($pos.index()));
62
+ }
63
+ }
64
+ return (0, _createClass2.default)(LayoutColumnView, [{
65
+ key: "ignoreMutation",
66
+ value: function ignoreMutation(mutation) {
67
+ // Ignore style attribute mutations — these are direct DOM writes during column drag
68
+ // (setting flex-basis for real-time resize feedback). Without this, PM's
69
+ // MutationObserver would immediately revert our style changes.
70
+ return mutation.type === 'attributes' && mutation.attributeName === 'style';
71
+ }
72
+ }]);
73
+ }();
11
74
  var _default = exports.default = function _default(options, pluginInjectionApi, portalProviderAPI, eventDispatcher) {
12
75
  return new _safePlugin.SafePlugin({
13
76
  key: pluginKey,
14
77
  props: {
15
- nodeViews: {
78
+ nodeViews: _objectSpread({
16
79
  layoutSection: function layoutSection(node, view, getPos) {
17
80
  return new _nodeviews.LayoutSectionView({
18
81
  node: node,
@@ -24,7 +87,11 @@ var _default = exports.default = function _default(options, pluginInjectionApi,
24
87
  options: options
25
88
  }).init();
26
89
  }
27
- }
90
+ }, (0, _experiments.editorExperiment)('platform_editor_layout_column_resize_handle', true) ? {
91
+ layoutColumn: function layoutColumn(node, view, getPos) {
92
+ return new LayoutColumnView(node, view, getPos);
93
+ }
94
+ } : {})
28
95
  }
29
96
  });
30
97
  };
@@ -308,6 +308,11 @@ function forceColumnWidths(state, tr, pos, presetLayout) {
308
308
  }
309
309
  return tr.replaceWith(pos + 1, pos + node.nodeSize - 1, columnWidth(node, state.schema, getWidthsForPreset(presetLayout)));
310
310
  }
311
+
312
+ /**
313
+ * Forces a layout section node to match the given preset layout by adjusting
314
+ * its column structure and widths, then restoring the original selection.
315
+ */
311
316
  export function forceSectionToPresetLayout(state, node, pos, presetLayout) {
312
317
  const forceColumnStructureFn = editorExperiment('advanced_layouts', true) ? forceColumnStructureNew : forceColumnStructure;
313
318
  let tr = forceColumnStructureFn(state, node, pos, presetLayout);
@@ -356,6 +361,13 @@ export const setPresetLayout = editorAnalyticsAPI => layout => (state, dispatch)
356
361
  };
357
362
  function layoutNeedChanges(node) {
358
363
  if (editorExperiment('advanced_layouts', true)) {
364
+ if (editorExperiment('platform_editor_layout_column_resize_handle', true)) {
365
+ // Custom widths that sum to 100% are valid and should not be forced back to presets
366
+ if (isValidLayoutWidthDistributions(node)) {
367
+ return false;
368
+ }
369
+ return true;
370
+ }
359
371
  return !getPresetLayout(node) || !isValidLayoutWidthDistributions(node);
360
372
  }
361
373
  return !getPresetLayout(node);
@@ -439,6 +451,11 @@ export const fixColumnStructure = state => {
439
451
  const node = state.doc.nodeAt(pos);
440
452
  if (node) {
441
453
  if (node.childCount !== getWidthsForPreset(selectedLayout).length) {
454
+ // If the resize handle experiment is on and widths are valid, don't force preset
455
+ // (column count mismatch might be from a different preset being selected)
456
+ if (editorExperiment('advanced_layouts', true) && editorExperiment('platform_editor_layout_column_resize_handle', true) && isValidLayoutWidthDistributions(node)) {
457
+ return;
458
+ }
442
459
  return forceSectionToPresetLayout(state, node, pos, selectedLayout);
443
460
  }
444
461
  if (!isValidLayoutWidthDistributions(node) && editorExperiment('advanced_layouts', true)) {