Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export default {
if (!this.hasLayoutMeta) {
this.onCreateProvision();
}
this.updateWidgetStore(this.parentOptions?.widgetId, 'layout:switch', this.layoutMode);
},
beforeUnmount() {
apos.bus.$off('widget-breadcrumb-operation', this.executeWidgetOperation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
ref="root"
class="apos-layout"
>
<section
<TransitionGroup
ref="grid"
name="apos-grid"
tag="section"
class="apos-layout__grid"
:class="gridClasses"
data-apos-test="aposLayoutContainer"
Expand Down Expand Up @@ -55,7 +57,7 @@
</template>
</div>
</div>
</section>
</TransitionGroup>
<AposGridManager
v-if="isManageMode"
:grid-state="gridState"
Expand All @@ -66,6 +68,8 @@
@resize-end="$emit('resize-end', $event); isResizing = false"
@move-start="isMoving = true"
@move-end="$emit('move-end', $event); isMoving = false"
@preview-move="onPreviewMove"
@preview-clear="onPreviewClear"
@add-fit-item="$emit('add-fit-item', $event)"
@patch-item="$emit('patch-item', $event)"
/>
Expand Down Expand Up @@ -117,7 +121,12 @@ export default {
data() {
return {
isResizing: false,
isMoving: false
isMoving: false,
// Live preview from manager: patches to apply to items for render-only
preview: {
patches: null,
key: null
}
};
},
computed: {
Expand All @@ -138,12 +147,16 @@ export default {
return slots;
},
renderItems() {
const base = this.gridState.current.items;
// Apply live preview patches if present (render-only)
if (Array.isArray(this.preview.patches) && this.preview.patches.length) {
return this.applyPreviewPatches(base, this.preview.patches);
}
if (!this.syntheticItems.length) {
return this.gridState.current.items;
return base;
}
// Merge and sort by order to ensure CSS order consistency
const merged = [
...this.gridState.current.items,
...base,
...this.syntheticItems
];
return merged.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
Expand All @@ -162,6 +175,61 @@ export default {
'is-moving': this.isMoving
};
}
},
methods: {
onPreviewMove({ patches, key }) {
if (!Array.isArray(patches) || !patches.length) {
this.onPreviewClear();
return;
}
if (this.preview.key === key) {
return;
}
this.preview = {
patches,
key
};
},
onPreviewClear() {
if (this.preview.patches || this.preview.key) {
this.preview = {
patches: null,
key: null
};
}
},
applyPreviewPatches(items, patches) {
// Map items by _id for quick lookup
const map = new Map(items.map((it, idx) => [ it._id, {
it,
idx
} ]));
const out = items.map(it => ({ ...it }));
for (const p of patches) {
const ref = map.get(p._id);
if (!ref) {
continue;
}
const target = out[ref.idx];
// Only apply layout-related fields present in the patch
if ('colstart' in p) {
target.colstart = p.colstart;
}
if ('rowstart' in p) {
target.rowstart = p.rowstart;
}
if ('colspan' in p) {
target.colspan = p.colspan;
}
if ('rowspan' in p) {
target.rowspan = p.rowspan;
}
if ('order' in p) {
target.order = p.order;
}
}
return out;
}
}
};
</script>
Expand Down Expand Up @@ -207,9 +275,19 @@ export default {

/* stylelint-disable-next-line media-feature-name-allowed-list */
@media (prefers-reduced-motion: no-preference) {
.apos-layout__grid {
transition: all 300ms ease;
/* TransitionGroup move/enter/leave animations for grid items */
.apos-grid-move {
transition: transform 200ms ease;
}
}

.apos-grid-enter-active,
.apos-grid-leave-active {
transition: opacity 200ms ease;
}

.apos-grid-enter-from,
.apos-grid-leave-to {
opacity: 0.01;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
}"
>
<div
v-if="!hasMotion"
data-shim
:data-id="item._id"
class="apos-layout__item-shim"
Expand Down Expand Up @@ -174,6 +175,9 @@ export default {
'resize-end',
'move-start',
'move-end',
// New: live preview events for layout rendering
'preview-move',
'preview-clear',
'add-fit-item',
'remove-item',
'patch-item'
Expand Down Expand Up @@ -234,8 +238,8 @@ export default {
iconSize: 11
},
onMoveDebounced: throttle(this.onMove, 10),
onResizeDebounced: throttle(this.onResize, 10)
// gridContentStyles: new Map()
onResizeDebounced: throttle(this.onResize, 10),
lastPreviewKey: null
};
},
computed: {
Expand Down Expand Up @@ -306,9 +310,13 @@ export default {
document.addEventListener('mouseup', this.onMouseUp);
document.addEventListener('touchend', this.onMouseUp);

const rootEl = this.$parent.$refs.root?.$el || this.$parent.$refs.root;
const gridRef = this.$parent.$refs.grid;
const gridEl = gridRef && gridRef.$el ? gridRef.$el : gridRef;

this.manager.init(
this.$parent.$refs.root,
this.$parent.$refs.grid,
rootEl,
gridEl,
(type, obj) => {
if (type === 'resize') {
this.sceneResizeIndex += 1;
Expand All @@ -325,7 +333,9 @@ export default {
// without the resize observer being properly cleaned up.
this.manager.onSceneResizeDebounced(entries);
});
this.resizeObserver.observe(this.$parent.$refs.grid);
if (gridEl) {
this.resizeObserver.observe(gridEl);
}

await this.$nextTick();
this.cloneCalculateIndex += 1;
Expand Down Expand Up @@ -408,6 +418,33 @@ export default {
this.ghostDataWrite.colstart = colstart ?? this.ghostDataWrite.colstart;
this.ghostDataWrite.colspan = colspan ?? this.ghostDataWrite.colspan;
this.ghostDataWrite.direction = direction ?? this.ghostDataWrite.direction;

// Live preview of resize result when direction is known
if (this.ghostDataWrite.direction && this.ghostDataWrite.id) {
const newKey = `${this.ghostDataWrite.colstart}:${this.ghostDataWrite.colspan}:${this.ghostDataWrite.direction}`;
if (this.lastPreviewKey !== newKey) {
const patches = this.manager.performItemResize({
data: this.ghostDataWrite,
state: this.gridState,
item: this.gridState.lookup.get(this.ghostDataWrite.id)
});
if (Array.isArray(patches) && patches.length) {
this.$emit('preview-move', { // reuse same preview channel
patches,
key: newKey
});
this.lastPreviewKey = newKey;
} else if (this.lastPreviewKey) {
// No effective change, clear existing preview
this.$emit('preview-clear');
this.lastPreviewKey = null;
}
}
} else if (this.lastPreviewKey) {
// Lost direction (e.g., mouse returned to start), clear preview
this.$emit('preview-clear');
this.lastPreviewKey = null;
}
}
},
onStartMove(item, event) {
Expand Down Expand Up @@ -476,6 +513,36 @@ export default {
if (colstart && rowstart) {
this.ghostDataWrite.colstart = colstart;
this.ghostDataWrite.rowstart = rowstart;
// Emit preview of would-be state when snap target changes
const newKey = `${colstart}:${rowstart}`;
if (
typeof this.ghostData.snapLeft === 'number' &&
typeof this.ghostData.snapTop === 'number'
) {
if (this.lastPreviewKey !== newKey) {
const patches = this.manager.performItemMove({
data: this.ghostDataWrite,
state: this.gridState,
item: this.gridState.lookup.get(this.ghostDataWrite.id),
precomp: this.movePrecomp
});
if (Array.isArray(patches) && patches.length) {
this.$emit('preview-move', {
patches,
key: newKey
});
this.lastPreviewKey = newKey;
} else if (this.lastPreviewKey) {
// No effective change, clear existing preview
this.$emit('preview-clear');
this.lastPreviewKey = null;
}
}
} else if (this.lastPreviewKey) {
// Lost snapping, clear preview
this.$emit('preview-clear');
this.lastPreviewKey = null;
}
}
},
onMouseMove(event) {
Expand Down Expand Up @@ -510,6 +577,11 @@ export default {
}
},
resetGhostData() {
// Always clear any preview when ghost state resets
if (this.lastPreviewKey) {
this.$emit('preview-clear');
this.lastPreviewKey = null;
}
Object.keys(this.ghostData).forEach(key => {
this.ghostData[key] = typeof this.ghostData[key] === 'boolean'
? false
Expand Down
33 changes: 29 additions & 4 deletions modules/@apostrophecms/layout-widget/ui/apos/lib/grid-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,21 @@ export class GridManager {
const maxStartX = Math.max(1, columns - colspan + 1);
const maxStartY = Math.max(1, rows - rowspan + 1);

// Initial nearest indices from current pixel position
let c = Math.round(left / stepX) + 1;
let r = Math.round(top / stepY) + 1;
// Initial nearest indices with custom snap threshold.
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
const tMoveOpt = (
state?.options?.snapThresholdMove ?? state?.options?.snapThreshold ?? 0.6
);
const tMove = Number(tMoveOpt);
const tMoveClamped = clamp(
Number.isFinite(tMove) ? tMove : 0.6,
0.05,
0.95
);
const shiftX = (1 - tMoveClamped) * stepX;
const shiftY = (1 - tMoveClamped) * stepY;
let c = Math.floor((left + shiftX) / stepX) + 1;
let r = Math.floor((top + shiftY) / stepY) + 1;
c = Math.max(1, Math.min(c, maxStartX));
r = Math.max(1, Math.min(r, maxStartY));

Expand Down Expand Up @@ -470,7 +482,20 @@ export class GridManager {
const columnWidth = containerRect.width / state.columns;
const direction = deltaX > 0 ? 'east' : 'west';
const directionCorrection = data.side === direction ? 1 : -1;
const deltaColspan = Math.round(Math.abs(deltaX) / columnWidth) * directionCorrection;
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
const tResizeOpt = (
state?.options?.snapThresholdResize ?? state?.options?.snapThreshold ?? 0.5
);
const tResize = Number(tResizeOpt);
const SNAP_THRESHOLD = clamp(
Number.isFinite(tResize) ? tResize : 0.5,
0.05,
0.95
);
const deltaSteps = Math.floor(
(Math.abs(deltaX) + (1 - SNAP_THRESHOLD) * columnWidth) / columnWidth
);
const deltaColspan = deltaSteps * directionCorrection;
const desired = Math.max(
state.options.minSpan,
Math.min(item.colspan + deltaColspan, state.columns)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export function itemsToState({
const resolvedOptions = {
...options,
columns: meta.columns || options.columns,
gap: [ 'layout', 'focus' ].includes(layoutMode) ? gap || '2px' : options.gap
gap: [ 'layout', 'focus' ].includes(layoutMode) ? gap || '2px' : options.gap,
snapThresholdMove: 0.7,
snapThreshold: 0.5
};

const positionsIndex = createPositionIndex(current.items, current.rows);
Expand Down