253 lines
9.1 KiB
JavaScript
253 lines
9.1 KiB
JavaScript
(function (root, factory) {
|
|
"use strict";
|
|
|
|
const api = Object.freeze(factory());
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
if (root) root.MbnMarketNavReorder = api;
|
|
})(typeof globalThis === "object" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
const fixedMarket = "dashboard";
|
|
const originalMarkets = Object.freeze([
|
|
"overseas", "exchange", "index", "kospi", "kosdaq",
|
|
"compare", "theme", "foreign-stock", "expert", "halted"
|
|
]);
|
|
const originalMarketSet = new Set(originalMarkets);
|
|
const dragMime = "application/x-mbn-market-nav";
|
|
const dragPrefix = "mbn-market-nav:v1:";
|
|
|
|
function isOriginalMarket(value) {
|
|
return typeof value === "string" && originalMarketSet.has(value);
|
|
}
|
|
|
|
function normalizeOrder(value) {
|
|
if (!Array.isArray(value) || value.length !== originalMarkets.length) return null;
|
|
const seen = new Set();
|
|
for (const market of value) {
|
|
if (!isOriginalMarket(market) || seen.has(market)) return null;
|
|
seen.add(market);
|
|
}
|
|
return Object.freeze([...value]);
|
|
}
|
|
|
|
// MainForm.swapTabPages exchanges the source and hovered TabPage. Keep the
|
|
// exact identity-based swap instead of deriving screen meaning from indices.
|
|
function swapOrder(order, sourceMarket, targetMarket) {
|
|
const normalized = normalizeOrder(order);
|
|
if (!normalized || !isOriginalMarket(sourceMarket) ||
|
|
!isOriginalMarket(targetMarket)) return null;
|
|
if (sourceMarket === targetMarket) {
|
|
return Object.freeze({ changed: false, order: normalized });
|
|
}
|
|
const sourceIndex = normalized.indexOf(sourceMarket);
|
|
const targetIndex = normalized.indexOf(targetMarket);
|
|
const next = [...normalized];
|
|
[next[sourceIndex], next[targetIndex]] = [next[targetIndex], next[sourceIndex]];
|
|
return Object.freeze({ changed: true, order: Object.freeze(next) });
|
|
}
|
|
|
|
function moveAdjacent(order, sourceMarket, direction) {
|
|
const normalized = normalizeOrder(order);
|
|
if (!normalized || !isOriginalMarket(sourceMarket) ||
|
|
!Number.isInteger(direction) || ![-1, 1].includes(direction)) return null;
|
|
const sourceIndex = normalized.indexOf(sourceMarket);
|
|
const targetIndex = sourceIndex + direction;
|
|
if (targetIndex < 0 || targetIndex >= normalized.length) {
|
|
return Object.freeze({ changed: false, order: normalized });
|
|
}
|
|
return swapOrder(normalized, sourceMarket, normalized[targetIndex]);
|
|
}
|
|
|
|
function createDragToken(market) {
|
|
if (!isOriginalMarket(market)) throw new TypeError("A reorderable market is required.");
|
|
return `${dragPrefix}${market}`;
|
|
}
|
|
|
|
function parseDragToken(value) {
|
|
if (typeof value !== "string" || !value.startsWith(dragPrefix)) return null;
|
|
const market = value.slice(dragPrefix.length);
|
|
return isOriginalMarket(market) && value === createDragToken(market) ? market : null;
|
|
}
|
|
|
|
function directMarketButton(nav, target, includeDashboard = false) {
|
|
if (!nav || !target || typeof target.closest !== "function") return null;
|
|
const button = target.closest("button[data-market]");
|
|
if (!button || button.parentElement !== nav) return null;
|
|
const market = button.dataset?.market;
|
|
if (isOriginalMarket(market) || (includeDashboard && market === fixedMarket)) return button;
|
|
return null;
|
|
}
|
|
|
|
function readOrder(nav) {
|
|
if (!nav || !nav.children) return null;
|
|
const buttons = Array.from(nav.children);
|
|
if (buttons.length !== originalMarkets.length + 1) return null;
|
|
const dashboard = directMarketButton(nav, buttons[0], true);
|
|
if (!dashboard || dashboard.dataset.market !== fixedMarket) return null;
|
|
return normalizeOrder(buttons.slice(1).map(button => {
|
|
const direct = directMarketButton(nav, button);
|
|
return direct?.dataset?.market;
|
|
}));
|
|
}
|
|
|
|
function applyOrder(nav, order) {
|
|
const normalized = normalizeOrder(order);
|
|
const current = readOrder(nav);
|
|
if (!normalized || !current) return false;
|
|
const byMarket = new Map(Array.from(nav.children).map(button => [
|
|
button.dataset.market,
|
|
button
|
|
]));
|
|
const dashboard = byMarket.get(fixedMarket);
|
|
if (!dashboard || typeof nav.append !== "function" || typeof nav.prepend !== "function") {
|
|
return false;
|
|
}
|
|
nav.prepend(dashboard);
|
|
for (const market of normalized) nav.append(byMarket.get(market));
|
|
return true;
|
|
}
|
|
|
|
function createController(options) {
|
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
throw new TypeError("Market-nav reorder options are required.");
|
|
}
|
|
const nav = options.nav;
|
|
if (!nav || typeof nav.addEventListener !== "function" ||
|
|
typeof nav.removeEventListener !== "function") {
|
|
throw new TypeError("A market nav element is required.");
|
|
}
|
|
const isBlocked = typeof options.isBlocked === "function" ? options.isBlocked : () => false;
|
|
const onReordered = typeof options.onReordered === "function" ? options.onReordered : () => {};
|
|
let mounted = false;
|
|
let draggingMarket = null;
|
|
|
|
function blocked() {
|
|
try {
|
|
return isBlocked() === true;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function reorder(result, reason, sourceButton) {
|
|
if (!result?.changed || !applyOrder(nav, result.order)) return false;
|
|
if (typeof sourceButton?.focus === "function") sourceButton.focus();
|
|
onReordered(Object.freeze({
|
|
reason,
|
|
order: result.order,
|
|
activeMarket: Array.from(nav.children)
|
|
.find(button => button.classList?.contains("active"))?.dataset?.market ?? null
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
function onDragStart(event) {
|
|
const button = directMarketButton(nav, event.target);
|
|
const market = button?.dataset?.market;
|
|
if (!button || blocked() || !event.dataTransfer) {
|
|
event.preventDefault?.();
|
|
draggingMarket = null;
|
|
return;
|
|
}
|
|
draggingMarket = market;
|
|
button.classList?.add("dragging");
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData(dragMime, createDragToken(market));
|
|
}
|
|
|
|
function onDragOver(event) {
|
|
if (blocked() || !draggingMarket || !directMarketButton(nav, event.target)) return;
|
|
event.preventDefault?.();
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
}
|
|
|
|
function clearDragging() {
|
|
draggingMarket = null;
|
|
for (const button of Array.from(nav.children)) button.classList?.remove("dragging");
|
|
}
|
|
|
|
function onDrop(event) {
|
|
const target = directMarketButton(nav, event.target);
|
|
if (!target || blocked() || !draggingMarket || !event.dataTransfer) {
|
|
clearDragging();
|
|
return;
|
|
}
|
|
event.preventDefault?.();
|
|
const sourceMarket = parseDragToken(event.dataTransfer.getData(dragMime));
|
|
if (!sourceMarket || sourceMarket !== draggingMarket) {
|
|
clearDragging();
|
|
return;
|
|
}
|
|
const order = readOrder(nav);
|
|
const source = Array.from(nav.children)
|
|
.find(button => button.dataset?.market === sourceMarket);
|
|
const result = swapOrder(order, sourceMarket, target.dataset.market);
|
|
reorder(result, "drag", source);
|
|
clearDragging();
|
|
}
|
|
|
|
function onKeyDown(event) {
|
|
if (!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
|
|
!["ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
const button = directMarketButton(nav, event.target);
|
|
if (!button) return;
|
|
event.preventDefault?.();
|
|
event.stopPropagation?.();
|
|
if (blocked() || event.repeat) return;
|
|
const direction = event.key === "ArrowUp" ? -1 : 1;
|
|
reorder(
|
|
moveAdjacent(readOrder(nav), button.dataset.market, direction),
|
|
"keyboard",
|
|
button);
|
|
}
|
|
|
|
function mount() {
|
|
if (mounted) return true;
|
|
if (!readOrder(nav)) return false;
|
|
for (const button of Array.from(nav.children)) {
|
|
const reorderable = isOriginalMarket(button.dataset?.market);
|
|
button.draggable = reorderable;
|
|
if (reorderable) {
|
|
button.setAttribute?.("aria-keyshortcuts", "Alt+ArrowUp Alt+ArrowDown");
|
|
button.setAttribute?.("title", "드래그 또는 Alt+화살표로 업무 탭 순서 변경");
|
|
}
|
|
}
|
|
nav.addEventListener("dragstart", onDragStart);
|
|
nav.addEventListener("dragover", onDragOver);
|
|
nav.addEventListener("drop", onDrop);
|
|
nav.addEventListener("dragend", clearDragging);
|
|
nav.addEventListener("keydown", onKeyDown);
|
|
mounted = true;
|
|
return true;
|
|
}
|
|
|
|
function destroy() {
|
|
if (!mounted) return;
|
|
nav.removeEventListener("dragstart", onDragStart);
|
|
nav.removeEventListener("dragover", onDragOver);
|
|
nav.removeEventListener("drop", onDrop);
|
|
nav.removeEventListener("dragend", clearDragging);
|
|
nav.removeEventListener("keydown", onKeyDown);
|
|
clearDragging();
|
|
mounted = false;
|
|
}
|
|
|
|
return Object.freeze({ mount, destroy, readOrder: () => readOrder(nav) });
|
|
}
|
|
|
|
return {
|
|
fixedMarket,
|
|
originalMarkets,
|
|
dragMime,
|
|
isOriginalMarket,
|
|
normalizeOrder,
|
|
swapOrder,
|
|
moveAdjacent,
|
|
createDragToken,
|
|
parseDragToken,
|
|
readOrder,
|
|
applyOrder,
|
|
createController
|
|
};
|
|
});
|