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
2 changes: 1 addition & 1 deletion .changeset/batch-registry-ownership-reads.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'grid-layout-plus': patch
---

Read all item ownership facts before updating registry state and styles, reducing repeated layout calculations for rejected items.
Reduce repeated browser layout calculations when validating item registration.
2 changes: 1 addition & 1 deletion .changeset/coalesce-registry-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'grid-layout-plus': patch
---

Coalesce GridItem registry validation into one pass per flush and look up layout items by id, removing a cubic slowdown with many items.
Reduce mounting and layout update overhead for grids with many items.
3 changes: 2 additions & 1 deletion .changeset/copy-sealed-layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
'grid-layout-plus': patch
---

Copy the engine's internal frozen layouts without re-validating every item, reducing per-drag-step cloning cost.
Reduce per-drag-step cloning cost by avoiding repeated validation of layouts already checked by
the engine.
2 changes: 1 addition & 1 deletion .changeset/keep-equal-item-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'grid-layout-plus': patch
---

Keep the previous style object when a GridItem's computed position style is unchanged, so moving one item no longer re-renders every item.
Avoid `GridItem` re-renders caused by unchanged position styles when another item moves.
5 changes: 0 additions & 5 deletions .changeset/report-registry-validation-errors.md

This file was deleted.

4 changes: 3 additions & 1 deletion .changeset/same-tick-col-num.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
'grid-layout-plus': patch
---

Validate a layout that changes in the same tick as `colNum` against the incoming column count, avoiding transient `invalid-layout` and `invalid-registration` errors.
Validate a layout that changes in the same tick as `colNum` against the incoming column count
when `responsive` is disabled, avoiding transient `invalid-layout` and `invalid-registration`
errors.
2 changes: 1 addition & 1 deletion .changeset/sweep-collision-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'grid-layout-plus': patch
---

Speed up overlap detection with a top-edge sweep before the pairwise diagnostic loop, especially for vertically stacked collision-free layouts.
Speed up overlap detection, especially for vertically stacked collision-free layouts.
2 changes: 1 addition & 1 deletion .changeset/write-item-position-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'grid-layout-plus': patch
---

Write GridItem position styles directly to the element instead of re-rendering the component on every move.
Avoid `GridItem` re-renders caused only by position changes while preserving custom style overrides.
69 changes: 69 additions & 0 deletions dev-server/e2e/placement-fixture.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed, defineComponent, h, onMounted, ref } from 'vue'

import type { Layout } from '../../src/helpers/types'

const preexisting = new URLSearchParams(location.search).get('preexisting') === 'true'
const showNewItem = ref(false)
const layout = ref<Layout>([
{ i: 'a', x: 0, y: 0, w: 2, h: 2 },
{ i: 'b', x: 2, y: 0, w: 2, h: 2 },
])
if (preexisting) layout.value.push({ i: 'c', x: 4, y: 0, w: 2, h: 2 })
const visibleItems = computed(() =>
layout.value.filter(item => item.i !== 'c' || showNewItem.value),
)
const transitions = ref(0)
const Probe = defineComponent({
setup() {
const root = ref<HTMLElement>()
onMounted(() => {
const item = root.value!.parentElement!
// 模拟图表等内容在 mounted 中测量布局,确保浏览器已计算首次样式。
getComputedStyle(item).getPropertyValue('transform')
item.getBoundingClientRect()
})
return () => h('span', { ref: root }, 'content')
},
})
function addItem() {
layout.value = preexisting
? layout.value.map(item => (item.i === 'c' ? { ...item, x: 6 } : item))
: [...layout.value, { i: 'c', x: 6, y: 0, w: 2, h: 2 }]
showNewItem.value = true
}
function moveItem() {
layout.value = layout.value.map(item => (item.i === 'c' ? { ...item, x: 8 } : item))
}
function recordTransition(event: TransitionEvent) {
if (event.propertyName === 'transform') transitions.value++
}
</script>

<template>
<div data-placement-fixture>
<button data-placement-add @click="addItem">
Add
</button>
<button data-placement-move @click="moveItem">
Move
</button>
<output data-placement-transitions>{{ transitions }}</output>
<GridLayout
:layout="layout"
:width="1200"
:col-num="12"
:row-height="30"
>
<GridItem
v-for="item in visibleItems"
:key="item.i"
:i="item.i"
:data-placement-item="item.i"
@transitionrun="recordTransition"
>
<Probe></Probe>
</GridItem>
</GridLayout>
</div>
</template>
7 changes: 7 additions & 0 deletions dev-server/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router'

import ContractFixture from './e2e/contract-fixture.vue'
import PlacementFixture from './e2e/placement-fixture.vue'

document.title = 'dev | Grid Layout Plus'

Expand All @@ -9,6 +10,12 @@ const demos = import.meta.glob('../docs/demos/*.vue')
export const router = createRouter({
history: createWebHashHistory('/'),
routes: [
{
path: '/__e2e/placement',
name: 'placement-fixture',
component: PlacementFixture,
meta: { internal: true },
},
{
path: '/__e2e/contracts/:phase',
name: 'contract-fixture',
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,9 @@ The component emits the current Layout and the complete breakpoint map with the
## Component methods or composables?

Use these methods when `GridLayout` renders the UI. Use [`useGridLayout`](./composables#usegridlayout) for headless state: its one-off methods commit synchronously and return `LayoutOperationResult`, so they use `accepted` instead of `pending`.

## GridItem component ref

`GridItem` exposes `wrapper`, its root element, and reactive `state`. Changes to `state.style`,
including individual properties, are reflected in the DOM with the same fallthrough-style precedence.
Layout updates may replace `state.style`; use the `style` attribute for persistent presentation overrides.
4 changes: 4 additions & 0 deletions docs/guide/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,10 @@ Programmatic layout and layer operations are documented in [Methods](./methods).

`GridItem` must be a descendant of `GridLayout`. Its `i` prop associates it with the matching `LayoutItem`; the parent Layout owns geometry, constraints, static state, per-item drag and resize flags, resize handles, and layer order.

HTML `class` and `style` attributes are forwarded to the root element. Custom styles keep Vue's
fallthrough precedence during layout updates and do not change the stored layout geometry.
New items appear without an initial positioning transition; later position changes animate normally.

### i

- type: `number | string`
Expand Down
5 changes: 5 additions & 0 deletions docs/zh/guide/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ function moveSummary() {
## 组件方法还是组合式函数?

由 `GridLayout` 渲染界面时使用这些组件方法。无头状态使用 [`useGridLayout`](./composables#usegridlayout):它的单次操作会同步提交并返回 `LayoutOperationResult`,所以状态是 `accepted`,而不是 `pending`。

## GridItem 组件引用

`GridItem` 暴露根元素 `wrapper` 和响应式 `state`。修改 `state.style`(包括单个属性)会同步到 DOM,
并保留相同的透传样式优先级。布局更新可能替换 `state.style`;需要持续生效的展示覆盖请使用 `style` 属性。
3 changes: 3 additions & 0 deletions docs/zh/guide/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ interface DropConfig<B extends string = DefaultBreakpoint> {

`GridItem` 必须位于 `GridLayout` 内。它通过 `i` 属性关联对应的 `LayoutItem`;几何信息、约束、静态状态、单项拖拽和缩放开关、缩放手柄以及层级顺序都由父级 Layout 维护。

HTML `class` 和 `style` 属性会透传到根元素。自定义样式在布局更新期间保留 Vue 的透传优先级,
并保持已存储的布局几何信息不变。新元素首次定位时直接出现,之后的位置变化保留正常过渡动画。

### i

- 类型:`number | string`
Expand Down
40 changes: 35 additions & 5 deletions scripts/benchmark.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { performance } from 'node:perf_hooks'

import { hasAnyCollision } from '../src/core/collision-sweep'
import {
fastHorizontalCompactor,
fastVerticalCompactor,
Expand Down Expand Up @@ -96,6 +97,16 @@ function highCandidateVertical(): Layout {
}))
}

function stackedVertical(): Layout {
let y = 0
return Array.from({ length: itemCount }, (_, index) => {
const h = 1 + (index % 7)
const item = { i: `stacked-${index}`, x: 0, y, w: 12, h }
y += h
return item
})
}

const cases: BenchmarkCase[] = [
{
name: 'vertical / sparse',
Expand Down Expand Up @@ -155,15 +166,15 @@ function resultChecksum(layout: Layout): number {
return checksum
}

function measure(run: () => Layout): TimingResult {
function measure<T>(run: () => T, checksumFor: (result: T) => number): TimingResult {
for (let index = 0; index < warmups; index++) run()

let checksum = 0
const durations = Array.from({ length: samples }, () => {
const start = performance.now()
const layout = run()
const result = run()
const duration = performance.now() - start
checksum ^= resultChecksum(layout)
checksum ^= checksumFor(result)
return duration
}).sort((first, second) => first - second)

Expand All @@ -181,8 +192,8 @@ function rounded(value: number): number {

function runCompactorBenchmarks(): void {
const rows = cases.flatMap(entry => {
const standard = measure(() => entry.standard.compact(entry.layout, entry.cols))
const indexed = measure(() => entry.indexed.compact(entry.layout, entry.cols))
const standard = measure(() => entry.standard.compact(entry.layout, entry.cols), resultChecksum)
const indexed = measure(() => entry.indexed.compact(entry.layout, entry.cols), resultChecksum)
return [
{
dataset: entry.name,
Expand All @@ -209,6 +220,24 @@ function runCompactorBenchmarks(): void {
console.table(rows)
}

function runCollisionBenchmark(): void {
const layout = stackedVertical()
const timing = measure(() => hasAnyCollision(layout), Number)

console.log(
`\nCollision sweep benchmark (${itemCount} items, ${warmups} warmups, ${samples} samples)`,
)
console.table([
{
dataset: 'vertical / stacked',
medianMs: rounded(timing.medianMs),
p95Ms: rounded(timing.p95Ms),
minMs: rounded(timing.minMs),
checksum: timing.checksum,
},
])
}

function heapUsed(): number {
return process.memoryUsage().heapUsed
}
Expand Down Expand Up @@ -293,4 +322,5 @@ function runInteractionBenchmark(): void {
console.log(`Grid Layout Plus benchmark profile: ${quick ? 'quick' : 'full'}`)
console.log('Results are evidence only and do not enforce machine-dependent timing thresholds.')
runCompactorBenchmarks()
runCollisionBenchmark()
runInteractionBenchmark()
Loading
Loading