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