Files
MBN_STOCK_WEBVIEW/Web/legacy-batch-selection-workflow.js

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
});
});