143 lines
5.3 KiB
JavaScript
143 lines
5.3 KiB
JavaScript
(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
|
|
});
|
|
});
|