@atlaskit/editor-plugin-layout
Version:
Layout plugin for @atlaskit/editor-core
364 lines (343 loc) • 14.9 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getColumnDividerDecorations = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _bindEventListener = require("bind-event-listener");
var _model = require("@atlaskit/editor-prosemirror/model");
var _view = require("@atlaskit/editor-prosemirror/view");
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; }
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; }
// Class names for the column resize divider widget — must stay in sync with layout.ts in editor-core
var layoutColumnDividerClassName = 'layout-column-divider';
var layoutColumnDividerRailClassName = 'layout-column-divider-rail';
var layoutColumnDividerThumbClassName = 'layout-column-divider-thumb';
// Minimum column width percentage to prevent columns from collapsing
var MIN_COLUMN_WIDTH_PERCENT = 5;
// Module-level drag state so it survives widget DOM recreation during transactions.
var dragState = null;
/**
* Dispatches a single undoable ProseMirror transaction to commit the final
* column widths after a drag completes.
*/
var dispatchColumnWidths = function dispatchColumnWidths(view, sectionPos, leftColIndex, leftWidth, rightWidth) {
var state = view.state;
var sectionNode = state.doc.nodeAt(sectionPos);
if (!sectionNode) {
return;
}
var layoutColumn = state.schema.nodes.layoutColumn;
var tr = state.tr;
var newColumns = [];
sectionNode.forEach(function (child, _offset, index) {
if (child.type === layoutColumn) {
var newWidth = child.attrs.width;
if (index === leftColIndex) {
newWidth = Number(leftWidth.toFixed(2));
} else if (index === leftColIndex + 1) {
newWidth = Number(rightWidth.toFixed(2));
}
newColumns.push(layoutColumn.create(_objectSpread(_objectSpread({}, child.attrs), {}, {
width: newWidth
}), child.content, child.marks));
} else {
newColumns.push(child);
}
});
tr.replaceWith(sectionPos + 1, sectionPos + sectionNode.nodeSize - 1, _model.Fragment.from(newColumns));
tr.setMeta('layoutColumnResize', true);
tr.setMeta('scrollIntoView', false);
view.dispatch(tr);
};
/**
* Calculates new column widths from the current mouse X position during drag.
* Uses the columns-only width cached at mousedown — no layout reflow.
*
* The denominator is `columnsWidth` (the total flex container width minus
* divider widgets and flex gaps) so that a 1 px mouse movement corresponds
* to the exact same visual shift in the column boundary, eliminating the
* few-pixel drift that occurred when using the full section width.
*/
var calcDragWidths = function calcDragWidths(clientX) {
if (!dragState) {
return null;
}
var _dragState = dragState,
columnsWidth = _dragState.columnsWidth;
if (columnsWidth === 0) {
return null;
}
var deltaX = clientX - dragState.startX;
var combinedWidth = dragState.startLeftWidth + dragState.startRightWidth;
var deltaPercent = deltaX / columnsWidth * 100;
var leftWidth = dragState.startLeftWidth + deltaPercent;
var rightWidth = dragState.startRightWidth - deltaPercent;
if (leftWidth < MIN_COLUMN_WIDTH_PERCENT) {
leftWidth = MIN_COLUMN_WIDTH_PERCENT;
rightWidth = combinedWidth - MIN_COLUMN_WIDTH_PERCENT;
} else if (rightWidth < MIN_COLUMN_WIDTH_PERCENT) {
rightWidth = MIN_COLUMN_WIDTH_PERCENT;
leftWidth = combinedWidth - MIN_COLUMN_WIDTH_PERCENT;
}
return {
leftWidth: leftWidth,
rightWidth: rightWidth
};
};
var onDragMouseMove = function onDragMouseMove(e) {
if (!dragState) {
return;
}
// If the mouse button was released outside the window (e.g. over browser chrome
// or an iframe), we won't receive a mouseup on ownerDoc. Detect this by checking
// whether any button is still held — if not, treat it as a drag end.
if (e.buttons === 0) {
onDragEnd(e.clientX);
return;
}
// Always capture the latest clientX so the rAF callback uses the most recent
// mouse position. Previously, intermediate positions were dropped when an rAF
// was already scheduled, causing the column boundary to lag behind the cursor.
dragState.lastClientX = e.clientX;
// If a paint frame is already scheduled it will pick up lastClientX — no need
// to schedule another one.
if (dragState.rafId !== null) {
return;
}
dragState.rafId = requestAnimationFrame(function () {
if (!dragState) {
return;
}
dragState.rafId = null;
var widths = calcDragWidths(dragState.lastClientX);
if (!widths) {
return;
}
// Write flex-basis directly onto the column elements' inline styles for immediate
// visual feedback. This beats PM's own inline flex-basis value without dispatching
// any PM transaction — keeping drag completely off the ProseMirror render path.
// The LayoutColumnView.ignoreMutation implementation ensures PM's MutationObserver
// does not revert these style changes mid-drag.
dragState.hasDragged = true;
dragState.leftColEl.style.flexBasis = "".concat(widths.leftWidth, "%");
dragState.rightColEl.style.flexBasis = "".concat(widths.rightWidth, "%");
});
};
/**
* Shared teardown for all drag-end paths (mouseup, missed mouseup detected via
* e.buttons===0 on mousemove, window blur, and visibilitychange). Commits the
* final column widths if a real drag occurred.
*/
var onDragEnd = function onDragEnd(clientX) {
if (!dragState) {
return;
}
var _dragState2 = dragState,
view = _dragState2.view,
sectionPos = _dragState2.sectionPos,
leftColIndex = _dragState2.leftColIndex,
leftColEl = _dragState2.leftColEl,
rightColEl = _dragState2.rightColEl,
hasDragged = _dragState2.hasDragged,
rafId = _dragState2.rafId,
startLeftWidth = _dragState2.startLeftWidth,
startRightWidth = _dragState2.startRightWidth,
unbindListeners = _dragState2.unbindListeners;
unbindListeners();
// Cancel any pending rAF so a stale frame doesn't write styles after teardown.
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
var ownerDoc = view.dom.ownerDocument;
ownerDoc.body.style.userSelect = '';
ownerDoc.body.style.cursor = '';
var widths = calcDragWidths(clientX);
dragState = null;
if (!hasDragged) {
// The user clicked without dragging — no flex-basis overrides were written,
// so there is nothing to clean up and no transaction to dispatch.
return;
}
// Clear the drag-time flex-basis overrides. The PM transaction below will
// write the committed widths back into the node attrs and re-render the DOM.
leftColEl.style.flexBasis = '';
rightColEl.style.flexBasis = '';
if (widths && (widths.leftWidth !== startLeftWidth || widths.rightWidth !== startRightWidth)) {
dispatchColumnWidths(view, sectionPos, leftColIndex, widths.leftWidth, widths.rightWidth);
}
};
var onDragMouseUp = function onDragMouseUp(e) {
onDragEnd(e.clientX);
};
/**
* Called when the window loses focus (blur) or the tab becomes hidden
* (visibilitychange). In either case the user can't be dragging anymore,
* so we commit the drag at the last known mouse position.
*/
var onDragCancel = function onDragCancel() {
if (!dragState) {
return;
}
// Commit at the last captured mouse position rather than startX, so the
// columns stay where the user last saw them.
onDragEnd(dragState.lastClientX);
};
/**
* Creates a column divider widget DOM element with drag-to-resize interaction for
* the adjacent layout columns. During drag, flex-basis is mutated directly on the
* column DOM elements for zero-overhead visual feedback (no PM transactions).
* A single undoable PM transaction is dispatched on mouseup to commit the final widths.
*/
var createColumnDividerWidget = function createColumnDividerWidget(view, sectionPos, columnIndex // index of the column to the RIGHT of this divider
) {
var ownerDoc = view.dom.ownerDocument;
// Outer container: wide transparent hit area for easy grabbing, zero flex footprint
var divider = ownerDoc.createElement('div');
divider.classList.add(layoutColumnDividerClassName);
divider.contentEditable = 'false';
// Rail: styled via layoutColumnDividerStyles in layout.ts
var rail = ownerDoc.createElement('div');
rail.classList.add(layoutColumnDividerRailClassName);
divider.appendChild(rail);
// Thumb: styled via layoutColumnDividerStyles in layout.ts
var thumb = ownerDoc.createElement('div');
thumb.classList.add(layoutColumnDividerThumbClassName);
rail.appendChild(thumb);
var leftColIndex = columnIndex - 1;
(0, _bindEventListener.bind)(divider, {
type: 'mousedown',
listener: function listener(e) {
var _ownerDoc$defaultView;
e.preventDefault();
e.stopPropagation();
var sectionNode = view.state.doc.nodeAt(sectionPos);
if (!sectionNode) {
return;
}
// Get the initial widths of the two adjacent columns
var leftCol = null;
var rightCol = null;
sectionNode.forEach(function (child, _offset, index) {
if (index === leftColIndex) {
leftCol = child;
}
if (index === leftColIndex + 1) {
rightCol = child;
}
});
if (!leftCol || !rightCol) {
return;
}
var sectionElement = divider.closest('[data-layout-section]');
if (!(sectionElement instanceof HTMLElement)) {
return;
}
// Capture the two adjacent column DOM elements upfront so mousemove can
// mutate their flex-basis directly without any PM transaction overhead.
// Query by data-layout-column-index (stamped by LayoutColumnView) rather than
// relying on positional order of [data-layout-column] elements, which would
// break if the DOM structure or ordering ever changes.
var leftColEl = sectionElement.querySelector("[data-layout-column-index=\"".concat(leftColIndex, "\"]"));
var rightColEl = sectionElement.querySelector("[data-layout-column-index=\"".concat(leftColIndex + 1, "\"]"));
if (!leftColEl || !rightColEl) {
return;
}
var unbindMove = (0, _bindEventListener.bind)(ownerDoc, {
type: 'mousemove',
listener: onDragMouseMove
});
var unbindUp = (0, _bindEventListener.bind)(ownerDoc, {
type: 'mouseup',
listener: onDragMouseUp
});
// If the user releases the mouse outside the browser window (e.g. over the
// OS desktop) and then brings the cursor back, we won't get a mouseup on
// ownerDoc. Listening on window for blur and on the document for
// visibilitychange catches tab switches and window focus loss respectively.
var unbindBlur = (0, _bindEventListener.bind)((_ownerDoc$defaultView = ownerDoc.defaultView) !== null && _ownerDoc$defaultView !== void 0 ? _ownerDoc$defaultView : window, {
type: 'blur',
listener: onDragCancel
});
var unbindVisibility = (0, _bindEventListener.bind)(ownerDoc, {
type: 'visibilitychange',
listener: onDragCancel
});
// Compute the width available to columns only (excluding divider widgets and
// flex gaps). Using this as the denominator ensures that a 1 px mouse delta
// translates to the exact pixel shift on the column boundary.
var sectionRect = sectionElement.getBoundingClientRect();
var dividers = sectionElement.querySelectorAll(".".concat(layoutColumnDividerClassName));
var dividersWidth = 0;
dividers.forEach(function (d) {
dividersWidth += d.getBoundingClientRect().width;
});
// Account for CSS gap between flex children. The gap is applied between
// every pair of direct children (columns + divider widgets).
var computedGap = parseFloat(getComputedStyle(sectionElement).gap || '0');
var childCount = sectionElement.children.length;
var totalGap = childCount > 1 ? computedGap * (childCount - 1) : 0;
var columnsWidth = sectionRect.width - dividersWidth - totalGap;
dragState = {
hasDragged: false,
lastClientX: e.clientX,
rafId: null,
view: view,
sectionPos: sectionPos,
leftColIndex: leftColIndex,
leftColEl: leftColEl,
rightColEl: rightColEl,
startX: e.clientX,
startLeftWidth: leftCol.attrs.width,
startRightWidth: rightCol.attrs.width,
columnsWidth: columnsWidth,
sectionElement: sectionElement,
unbindListeners: function unbindListeners() {
unbindMove();
unbindUp();
unbindBlur();
unbindVisibility();
}
};
ownerDoc.body.style.userSelect = 'none';
ownerDoc.body.style.cursor = 'col-resize';
}
});
return divider;
};
/**
* Returns ProseMirror Decoration widgets for column dividers between layout columns.
* Each divider supports drag-to-resize interaction for the adjacent columns.
*/
var getColumnDividerDecorations = exports.getColumnDividerDecorations = function getColumnDividerDecorations(state, view) {
var decorations = [];
if (!view) {
return decorations;
}
var layoutSection = state.schema.nodes.layoutSection;
state.doc.descendants(function (node, pos) {
if (node.type === layoutSection) {
// Walk through layout column children and add dividers between them
node.forEach(function (child, offset, index) {
// Add a divider widget BEFORE every column except the first
if (index > 0) {
var sectionPos = pos;
var colIndex = index;
var widgetPos = pos + offset + 1; // position at the start of this column
decorations.push(_view.Decoration.widget(widgetPos, function () {
return createColumnDividerWidget(view, sectionPos, colIndex);
}, {
side: -1,
// place before the position
key: "layout-col-divider-".concat(pos, "-").concat(index),
ignoreSelection: true
}));
}
});
return false; // don't descend into children
}
return true; // continue descending
});
return decorations;
};