mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor(studio): extract shared layerOrdering module from LayersPanel
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../utils/studioHelpers";
|
||||
import { Layers } from "../../icons/SystemIcons";
|
||||
import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
|
||||
import { computeReorderZValues, getElementZIndex } from "../../player/lib/layerOrdering";
|
||||
|
||||
const TAG_ICONS: Record<string, string> = {
|
||||
video: "Vi",
|
||||
@@ -228,9 +229,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
||||
reordered.splice(toIndex, 0, moved);
|
||||
|
||||
const existingValues = siblingLayers.map((l) => getElementZIndex(l.element));
|
||||
const sorted = [...existingValues].sort((a, b) => b - a);
|
||||
const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
|
||||
const zValues = hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
|
||||
const zValues = computeReorderZValues(existingValues, fromIndex, toIndex);
|
||||
|
||||
const entries = reordered.map((layer, i) => ({
|
||||
element: layer.element,
|
||||
@@ -388,25 +387,6 @@ export const LayersPanel = memo(function LayersPanel() {
|
||||
|
||||
// ── Pure helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function getElementZIndex(element: HTMLElement): number {
|
||||
try {
|
||||
const inline = element.style?.zIndex;
|
||||
if (inline && inline !== "auto") {
|
||||
const parsed = parseInt(inline, 10);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
const win = element.ownerDocument?.defaultView;
|
||||
if (!win) return 0;
|
||||
const value = win.getComputedStyle(element).zIndex;
|
||||
if (value === "auto" || value === "") return 0;
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
|
||||
if (layers.length <= 1) return layers;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeReorderZValues, getElementZIndex, resolveContextOrder } from "./layerOrdering";
|
||||
|
||||
function makeElement(zIndex?: string): HTMLElement {
|
||||
const element = document.createElement("div");
|
||||
if (zIndex != null) element.style.zIndex = zIndex;
|
||||
document.body.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
describe("getElementZIndex", () => {
|
||||
it("returns inline z-index when present", () => {
|
||||
expect(getElementZIndex(makeElement("7"))).toBe(7);
|
||||
});
|
||||
|
||||
it("falls back to computed style when inline z-index is not usable", () => {
|
||||
const element = makeElement();
|
||||
element.className = "computed-z";
|
||||
const style = document.createElement("style");
|
||||
style.textContent = ".computed-z { position: relative; z-index: 12; }";
|
||||
document.head.appendChild(style);
|
||||
|
||||
expect(getElementZIndex(element)).toBe(12);
|
||||
});
|
||||
|
||||
it("returns zero for auto or missing z-index", () => {
|
||||
expect(getElementZIndex(makeElement())).toBe(0);
|
||||
expect(getElementZIndex(makeElement("auto"))).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeReorderZValues", () => {
|
||||
it("preserves distinct existing z-index values and remaps them onto the new order", () => {
|
||||
expect(computeReorderZValues([1, 8, 3], 0, 2)).toEqual([8, 3, 1]);
|
||||
});
|
||||
|
||||
it("renumbers an all-tied group to descending contiguous z-index values", () => {
|
||||
expect(computeReorderZValues([0, 0, 0], 2, 0)).toEqual([3, 2, 1]);
|
||||
});
|
||||
|
||||
it("renumbers the whole group when any existing z-index values are tied", () => {
|
||||
expect(computeReorderZValues([5, 5, 1], 2, 0)).toEqual([3, 2, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveContextOrder", () => {
|
||||
it("sorts a flat sibling group by z-index descending then original order", () => {
|
||||
const ordered = resolveContextOrder([
|
||||
{ id: "a", zIndex: 2, parentCompositionId: null, compositionAncestors: ["root"] },
|
||||
{ id: "b", zIndex: 5, parentCompositionId: null, compositionAncestors: ["root"] },
|
||||
{ id: "c", zIndex: 5, parentCompositionId: null, compositionAncestors: ["root"] },
|
||||
]);
|
||||
|
||||
expect(ordered.map((item) => item.id)).toEqual(["b", "c", "a"]);
|
||||
});
|
||||
|
||||
it("keeps distinct stacking contexts from interleaving", () => {
|
||||
const ordered = resolveContextOrder([
|
||||
{ id: "root-low", zIndex: 1, parentCompositionId: null, compositionAncestors: ["root"] },
|
||||
{
|
||||
id: "nested-high",
|
||||
zIndex: 100,
|
||||
parentCompositionId: "scene",
|
||||
compositionAncestors: ["root", "scene"],
|
||||
},
|
||||
{ id: "root-top", zIndex: 2, parentCompositionId: null, compositionAncestors: ["root"] },
|
||||
]);
|
||||
|
||||
expect(ordered.map((item) => item.id)).toEqual(["root-top", "root-low", "nested-high"]);
|
||||
});
|
||||
|
||||
it("returns an empty list for empty input", () => {
|
||||
expect(resolveContextOrder([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
export interface StackingContextDescriptor {
|
||||
parentCompositionId: string | null;
|
||||
compositionAncestors: readonly string[];
|
||||
stackingContextId?: string | null;
|
||||
}
|
||||
|
||||
export interface ContextOrderItem extends StackingContextDescriptor {
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function getElementZIndex(element: HTMLElement): number {
|
||||
try {
|
||||
const inline = element.style?.zIndex;
|
||||
if (inline && inline !== "auto") {
|
||||
const parsed = parseInt(inline, 10);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
const win = element.ownerDocument?.defaultView;
|
||||
if (!win) return 0;
|
||||
const value = win.getComputedStyle(element).zIndex;
|
||||
if (value === "auto" || value === "") return 0;
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function computeReorderZValues(
|
||||
existingValues: readonly number[],
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
): number[] {
|
||||
const reordered = [...existingValues];
|
||||
const [moved] = reordered.splice(fromIndex, 1);
|
||||
reordered.splice(toIndex, 0, moved);
|
||||
|
||||
const sorted = [...existingValues].sort((a, b) => b - a);
|
||||
const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
|
||||
return hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
|
||||
}
|
||||
|
||||
// Exported in a later unit when the timeline consumes it; internal-only for now.
|
||||
function resolveStackingContextKey(item: StackingContextDescriptor): string {
|
||||
return item.stackingContextId ?? item.parentCompositionId ?? item.compositionAncestors[0] ?? "";
|
||||
}
|
||||
|
||||
function resolveStackingContextDepth(item: StackingContextDescriptor): number {
|
||||
const contextKey = resolveStackingContextKey(item);
|
||||
if (!contextKey) return 0;
|
||||
const index = item.compositionAncestors.indexOf(contextKey);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
export function resolveContextOrder<T extends ContextOrderItem>(items: readonly T[]): T[] {
|
||||
if (items.length === 0) return [];
|
||||
|
||||
const groups = new Map<
|
||||
string,
|
||||
{ firstIndex: number; depth: number; entries: Array<{ item: T; index: number }> }
|
||||
>();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const key = resolveStackingContextKey(item);
|
||||
const group = groups.get(key);
|
||||
if (group) {
|
||||
group.entries.push({ item, index });
|
||||
return;
|
||||
}
|
||||
groups.set(key, {
|
||||
firstIndex: index,
|
||||
depth: resolveStackingContextDepth(item),
|
||||
entries: [{ item, index }],
|
||||
});
|
||||
});
|
||||
|
||||
return [...groups.values()]
|
||||
.sort((a, b) => a.depth - b.depth || a.firstIndex - b.firstIndex)
|
||||
.flatMap((group) =>
|
||||
group.entries
|
||||
.sort((a, b) => b.item.zIndex - a.item.zIndex || a.index - b.index)
|
||||
.map((entry) => entry.item),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user