Migrate remaining legacy operator workflows
This commit is contained in:
142
Web/legacy-batch-selection-workflow.js
Normal file
142
Web/legacy-batch-selection-workflow.js
Normal file
@@ -0,0 +1,142 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
const api = Object.freeze(factory());
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
if (root) root.MbnLegacyBatchSelectionWorkflow = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const MAX_ITEMS = 2048;
|
||||
const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
||||
|
||||
function requireItemCount(value) {
|
||||
const count = Number(value);
|
||||
if (!Number.isInteger(count) || count < 0 || count > MAX_ITEMS) {
|
||||
throw new RangeError(`The selectable item count must be from 0 through ${MAX_ITEMS}.`);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function requireIndex(value, count, name) {
|
||||
const index = Number(value);
|
||||
if (!Number.isInteger(index) || index < 0 || index >= count) {
|
||||
throw new RangeError(`${name} is outside the selectable item range.`);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function normalizeSelectedIndices(values, count) {
|
||||
if (!Array.isArray(values)) throw new TypeError("Selected indices must be an array.");
|
||||
const unique = new Set();
|
||||
for (const value of values) unique.add(requireIndex(value, count, "A selected index"));
|
||||
return [...unique].sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
function normalizeAnchor(value, count) {
|
||||
if (value === undefined || value === null || value === -1) return -1;
|
||||
return requireIndex(value, count, "The selection anchor");
|
||||
}
|
||||
|
||||
function createSelection(itemCount, selectedIndices = [], anchorIndex = -1) {
|
||||
const count = requireItemCount(itemCount);
|
||||
const selected = normalizeSelectedIndices(selectedIndices, count);
|
||||
const anchor = normalizeAnchor(anchorIndex, count);
|
||||
return Object.freeze({
|
||||
itemCount: count,
|
||||
selectedIndices: Object.freeze(selected),
|
||||
anchorIndex: anchor
|
||||
});
|
||||
}
|
||||
|
||||
function inclusiveRange(left, right) {
|
||||
const from = Math.min(left, right);
|
||||
const to = Math.max(left, right);
|
||||
return Array.from({ length: to - from + 1 }, (_, offset) => from + offset);
|
||||
}
|
||||
|
||||
// Mirrors MainForm.listView1_MouseDown: plain click replaces, Ctrl toggles,
|
||||
// Shift replaces with an anchor range, and Ctrl+Shift unions that range.
|
||||
// The clicked row becomes the next anchor after every successful gesture.
|
||||
function applyPointerSelection(selection, targetIndex, modifiers = {}) {
|
||||
if (!selection || typeof selection !== "object" || Array.isArray(selection)) {
|
||||
throw new TypeError("A legacy selection snapshot is required.");
|
||||
}
|
||||
const current = createSelection(
|
||||
selection.itemCount,
|
||||
selection.selectedIndices,
|
||||
selection.anchorIndex);
|
||||
const target = requireIndex(targetIndex, current.itemCount, "The target index");
|
||||
if (!modifiers || typeof modifiers !== "object" || Array.isArray(modifiers)) {
|
||||
throw new TypeError("Selection modifiers must be an object.");
|
||||
}
|
||||
const control = modifiers.ctrlKey === true;
|
||||
const shift = modifiers.shiftKey === true;
|
||||
const anchor = current.anchorIndex >= 0 ? current.anchorIndex : target;
|
||||
let selected;
|
||||
|
||||
if (control && shift) {
|
||||
selected = [...new Set([...current.selectedIndices, ...inclusiveRange(anchor, target)])]
|
||||
.sort((left, right) => left - right);
|
||||
} else if (control) {
|
||||
const values = new Set(current.selectedIndices);
|
||||
if (values.has(target)) values.delete(target);
|
||||
else values.add(target);
|
||||
selected = [...values].sort((left, right) => left - right);
|
||||
} else if (shift) {
|
||||
selected = inclusiveRange(anchor, target);
|
||||
} else {
|
||||
selected = [target];
|
||||
}
|
||||
|
||||
return createSelection(current.itemCount, selected, target);
|
||||
}
|
||||
|
||||
function canActivateSingle(selection, targetIndex) {
|
||||
if (!selection || typeof selection !== "object" || Array.isArray(selection)) return false;
|
||||
try {
|
||||
const current = createSelection(
|
||||
selection.itemCount,
|
||||
selection.selectedIndices,
|
||||
selection.anchorIndex);
|
||||
const target = requireIndex(targetIndex, current.itemCount, "The target index");
|
||||
return current.selectedIndices.length === 1 && current.selectedIndices[0] === target;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Materialize the complete batch before the caller mutates its playlist. A
|
||||
// validation error therefore cannot leave an operator-visible partial batch.
|
||||
function materializeAtomicBatch(items, materialize) {
|
||||
if (!Array.isArray(items) || items.length < 1 || items.length > MAX_ITEMS) {
|
||||
throw new RangeError(`A batch must contain from 1 through ${MAX_ITEMS} items.`);
|
||||
}
|
||||
if (typeof materialize !== "function") {
|
||||
throw new TypeError("A batch materializer is required.");
|
||||
}
|
||||
const entries = [];
|
||||
const ids = new Set();
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const entry = materialize(items[index], index);
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
throw new TypeError("Every materialized batch item must be a playlist entry object.");
|
||||
}
|
||||
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
||||
if (!identifierPattern.test(id) || ids.has(id)) {
|
||||
throw new TypeError("Every materialized batch item must have a unique safe id.");
|
||||
}
|
||||
ids.add(id);
|
||||
entries.push(entry);
|
||||
}
|
||||
return Object.freeze(entries);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
MAX_ITEMS,
|
||||
createSelection,
|
||||
applyPointerSelection,
|
||||
canActivateSingle,
|
||||
materializeAtomicBatch
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user