Skip to content
Open
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
44 changes: 35 additions & 9 deletions prototype/canvas-layer-controls.html
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,29 @@ <h2 style="margin-top:18px;">Adapts when reused</h2>
return "";
}

// Locking a layer's stated purpose is "so it cannot move by accident" (see the summary
// note above the list). That promise covers two paths: the locked layer's own up/down
// controls, and an unlocked neighbour's move that would swap positions with it. A single
// up/down step only ever swaps with the adjacent layer, so checking that one neighbour is
// enough to decide whether a move is allowed.
function canMoveLayer(list, index, delta) {
const target = index + delta;
if (target < 0 || target >= list.length) { return false; }
const layer = list[index];
const neighbor = list[target];
if (!layer || !neighbor) { return false; }
return !layer.locked && !neighbor.locked;
}

function reorderLayers(list, index, delta) {
if (!canMoveLayer(list, index, delta)) { return list; }
const target = index + delta;
const copy = list.slice();
const item = copy.splice(index, 1)[0];
copy.splice(target, 0, item);
return copy;
}

function renderLayer(layer, index) {
const type = LAYER_TYPES[layer.type];

Expand All @@ -368,10 +391,10 @@ <h2 style="margin-top:18px;">Adapts when reused</h2>
const main = el("div", { class: "layer-main" }, [nameRow, meta]);

const up = el("button", { type: "button", class: "tiny secondary", "aria-label": "Move " + type.label + " up", text: "▲" }, []);
if (index === 0) { up.disabled = true; }
if (!canMoveLayer(layers, index, -1)) { up.disabled = true; }
up.addEventListener("click", function () { move(index, -1); });
const down = el("button", { type: "button", class: "tiny secondary", "aria-label": "Move " + type.label + " down", text: "▼" }, []);
if (index === layers.length - 1) { down.disabled = true; }
if (!canMoveLayer(layers, index, 1)) { down.disabled = true; }
down.addEventListener("click", function () { move(index, 1); });

const vis = el("button", { type: "button", class: "tiny secondary", text: layer.visible ? "Hide" : "Show" }, []);
Expand Down Expand Up @@ -403,17 +426,20 @@ <h2 style="margin-top:18px;">Adapts when reused</h2>
}

function move(index, delta) {
const target = index + delta;
if (target < 0 || target >= layers.length) { return; }
const copy = layers.slice();
const item = copy.splice(index, 1)[0];
copy.splice(target, 0, item);
layers = copy;
const next = reorderLayers(layers, index, delta);
if (next === layers) { return; }
layers = next;
render();
}

if (typeof module !== "undefined" && module.exports) {
module.exports = { evaluate: evaluate, LAYER_TYPES: LAYER_TYPES, layoutSignature: layoutSignature };
module.exports = {
evaluate: evaluate,
LAYER_TYPES: LAYER_TYPES,
layoutSignature: layoutSignature,
canMoveLayer: canMoveLayer,
reorderLayers: reorderLayers,
};
}

render();
Expand Down
42 changes: 41 additions & 1 deletion prototype/canvas-layer-controls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,44 @@ if (typeof M.layoutSignature === "function") {
);
}

console.log("canvas layer controls: stack guardrails (cover, lock, hidden speaker) verified");
// Locking a layer's stated purpose is "so it cannot move by accident" (see the lede /
// summary-note copy). That must hold both when the locked layer's own controls are used and
// when an unlocked neighbour's move would swap positions with it.
assert.ok(typeof M.canMoveLayer === "function", "prototype exports canMoveLayer()");
assert.ok(typeof M.reorderLayers === "function", "prototype exports reorderLayers()");

const lockedStack = [
{ id: "captions", type: "captions", visible: true, locked: false },
{ id: "brand", type: "brand", visible: true, locked: false },
{ id: "background", type: "background", visible: true, locked: true },
];

assert.strictEqual(M.canMoveLayer(lockedStack, 2, -1), false, "the locked layer itself cannot move up");
assert.strictEqual(M.canMoveLayer(lockedStack, 1, 1), false, "an unlocked neighbour cannot displace the locked layer by moving down");
assert.strictEqual(M.canMoveLayer(lockedStack, 0, 1), true, "two unlocked layers may still swap with each other");

assert.strictEqual(
M.reorderLayers(lockedStack, 2, -1),
lockedStack,
"moving a locked layer is refused and returns the same list reference",
);
assert.strictEqual(
M.reorderLayers(lockedStack, 1, 1),
lockedStack,
"moving an unlocked layer into a locked neighbour is refused",
);

const reordered = M.reorderLayers(lockedStack, 0, 1);
assert.notStrictEqual(reordered, lockedStack, "an allowed reorder returns a new list");
assert.deepStrictEqual(
reordered.map((layer) => layer.id),
["brand", "captions", "background"],
"an allowed reorder swaps the requested neighbour",
);
assert.deepStrictEqual(
lockedStack.map((layer) => layer.id),
["captions", "brand", "background"],
"reorderLayers does not mutate its input list",
);

console.log("canvas layer controls: stack guardrails (cover, lock, hidden speaker, locked reorder) verified");