feat: restore legacy drag events and Tornado launch integration
This commit is contained in:
@@ -61,6 +61,8 @@
|
||||
const operatorCatalogCode = document.getElementById("operator-catalog-code");
|
||||
const operatorCatalogNameLabel = document.getElementById("operator-catalog-name-label");
|
||||
const operatorCatalogName = document.getElementById("operator-catalog-name");
|
||||
const operatorCatalogNameCheck = document.getElementById("operator-catalog-name-check");
|
||||
const operatorCatalogNameStatus = document.getElementById("operator-catalog-name-status");
|
||||
const operatorCatalogStockQuery = document.getElementById("operator-catalog-stock-query");
|
||||
const operatorCatalogStockSearch = document.getElementById("operator-catalog-stock-search");
|
||||
const operatorCatalogStockResults = document.getElementById("operator-catalog-stock-results");
|
||||
@@ -87,11 +89,36 @@
|
||||
let namedPlaylistResultClickTimer = null;
|
||||
let pendingNamedPlaylistCommand = null;
|
||||
let lastCommandSequence = 0;
|
||||
const selectedCutsDragToken = "legacy-selected-cuts";
|
||||
const playlistDragMime = "text/x-mbn-playlist-row";
|
||||
const operatorCatalogDragMime = "text/x-mbn-operator-catalog-row";
|
||||
let draggedPlaylistRowId = null;
|
||||
let deferredPlaylistPlainSelectionRowId = null;
|
||||
let playlistDragBlockedRowId = null;
|
||||
let playlistMutationLocked = false;
|
||||
let draggedTabId = null;
|
||||
let tabDragLastTargetId = null;
|
||||
let pendingTabHoverSwap = null;
|
||||
let draggedOperatorCatalogRowId = null;
|
||||
let operatorCatalogDragBlockedRowId = null;
|
||||
let operatorCatalogMutationLocked = false;
|
||||
|
||||
function send(type, payload) {
|
||||
if (webview && !uiBusy) webview.postMessage({ type: type, payload: payload || {} });
|
||||
}
|
||||
|
||||
function isOperatorMutationLocked(state) {
|
||||
const playout = state && state.playout;
|
||||
return !state || state.isBusy === true || state.fixedSectionBatch != null ||
|
||||
(playout && (playout.isBusy === true || playout.outcomeUnknown === true ||
|
||||
playout.isPlayCompletionPending === true || playout.phase !== "idle"));
|
||||
}
|
||||
|
||||
function dragDropPosition(event, element) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return event.clientY < bounds.top + (bounds.height / 2) ? "before" : "after";
|
||||
}
|
||||
|
||||
function clearDelayedClick(timer) {
|
||||
if (timer !== null) window.clearTimeout(timer);
|
||||
}
|
||||
@@ -166,7 +193,7 @@
|
||||
element.addEventListener("dragstart", function (event) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", "legacy-selected-cuts");
|
||||
event.dataTransfer.setData("text/plain", selectedCutsDragToken);
|
||||
}
|
||||
});
|
||||
return element;
|
||||
@@ -197,6 +224,83 @@
|
||||
}
|
||||
}
|
||||
|
||||
function clearPlaylistDragVisuals() {
|
||||
playlistRows.querySelectorAll(".dragging, .drag-before, .drag-after")
|
||||
.forEach(function (row) {
|
||||
row.classList.remove("dragging", "drag-before", "drag-after");
|
||||
});
|
||||
}
|
||||
|
||||
function onPlaylistDragStart(event) {
|
||||
const row = event.currentTarget;
|
||||
const rowId = row.dataset.rowId;
|
||||
if (playlistMutationLocked || !rowId || !event.dataTransfer ||
|
||||
playlistDragBlockedRowId === rowId || event.target.closest("input, button")) {
|
||||
playlistDragBlockedRowId = null;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
playlistDragBlockedRowId = null;
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
draggedPlaylistRowId = rowId;
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData(playlistDragMime, rowId);
|
||||
row.classList.add("dragging");
|
||||
}
|
||||
|
||||
function onPlaylistDragOver(event) {
|
||||
const target = event.currentTarget;
|
||||
if (playlistMutationLocked || !draggedPlaylistRowId ||
|
||||
target.dataset.rowId === draggedPlaylistRowId) return;
|
||||
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
const position = dragDropPosition(event, target);
|
||||
target.classList.toggle("drag-before", position === "before");
|
||||
target.classList.toggle("drag-after", position === "after");
|
||||
}
|
||||
|
||||
function onPlaylistDragLeave(event) {
|
||||
event.currentTarget.classList.remove("drag-before", "drag-after");
|
||||
}
|
||||
|
||||
function onPlaylistDrop(event) {
|
||||
const target = event.currentTarget;
|
||||
const sourceRowId = draggedPlaylistRowId;
|
||||
const targetRowId = target.dataset.rowId;
|
||||
if (playlistMutationLocked || !sourceRowId || !targetRowId ||
|
||||
sourceRowId === targetRowId || !event.dataTransfer) {
|
||||
clearPlaylistDragVisuals();
|
||||
draggedPlaylistRowId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const token = event.dataTransfer.getData(playlistDragMime);
|
||||
if (token !== sourceRowId) {
|
||||
clearPlaylistDragVisuals();
|
||||
draggedPlaylistRowId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const position = dragDropPosition(event, target);
|
||||
clearPlaylistDragVisuals();
|
||||
draggedPlaylistRowId = null;
|
||||
send("reorder-playlist-rows", {
|
||||
sourceRowId: sourceRowId,
|
||||
targetRowId: targetRowId,
|
||||
position: position
|
||||
});
|
||||
}
|
||||
|
||||
function onPlaylistDragEnd() {
|
||||
draggedPlaylistRowId = null;
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
playlistDragBlockedRowId = null;
|
||||
clearPlaylistDragVisuals();
|
||||
}
|
||||
|
||||
function createPlaylistElement(row) {
|
||||
const element = document.createElement("div");
|
||||
element.className = "playlist-row playlist-data-row";
|
||||
@@ -222,18 +326,67 @@
|
||||
}
|
||||
|
||||
element.addEventListener("pointerdown", function (event) {
|
||||
if (event.button !== 0 || enabledCell.contains(event.target)) return;
|
||||
if (event.button !== 0) return;
|
||||
if (enabledCell.contains(event.target)) {
|
||||
// Native dragstart targets the nearest draggable ancestor (the row),
|
||||
// not necessarily the checkbox originally pressed. Remember the
|
||||
// pointer origin so a checkbox gesture cannot reorder the row.
|
||||
playlistDragBlockedRowId = element.dataset.rowId;
|
||||
return;
|
||||
}
|
||||
playlistDragBlockedRowId = null;
|
||||
element.focus({ preventScroll: true });
|
||||
const plainSelection = !event.ctrlKey && !event.shiftKey;
|
||||
const preserveSelectedBlock = plainSelection &&
|
||||
element.getAttribute("aria-selected") === "true" &&
|
||||
playlistRows.querySelectorAll('.playlist-data-row[aria-selected="true"]').length > 1;
|
||||
if (preserveSelectedBlock) {
|
||||
deferredPlaylistPlainSelectionRowId = element.dataset.rowId;
|
||||
return;
|
||||
}
|
||||
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
send("select-playlist-row", {
|
||||
rowId: element.dataset.rowId,
|
||||
control: event.ctrlKey,
|
||||
shift: event.shiftKey
|
||||
});
|
||||
});
|
||||
element.addEventListener("pointerup", function (event) {
|
||||
if (enabledCell.contains(event.target)) {
|
||||
playlistDragBlockedRowId = null;
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0 ||
|
||||
deferredPlaylistPlainSelectionRowId !== element.dataset.rowId) return;
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
send("select-playlist-row", {
|
||||
rowId: element.dataset.rowId,
|
||||
control: false,
|
||||
shift: false
|
||||
});
|
||||
});
|
||||
element.addEventListener("pointercancel", function () {
|
||||
if (playlistDragBlockedRowId === element.dataset.rowId) {
|
||||
playlistDragBlockedRowId = null;
|
||||
}
|
||||
});
|
||||
element.addEventListener("dragstart", onPlaylistDragStart);
|
||||
element.addEventListener("dragover", onPlaylistDragOver);
|
||||
element.addEventListener("dragleave", onPlaylistDragLeave);
|
||||
element.addEventListener("drop", onPlaylistDrop);
|
||||
element.addEventListener("dragend", onPlaylistDragEnd);
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderPlaylist(state) {
|
||||
playlistMutationLocked = isOperatorMutationLocked(state);
|
||||
if (playlistMutationLocked) {
|
||||
draggedPlaylistRowId = null;
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
playlistDragBlockedRowId = null;
|
||||
clearPlaylistDragVisuals();
|
||||
}
|
||||
const existingRows = new Map();
|
||||
Array.from(playlistRows.children).forEach(function (element) {
|
||||
existingRows.set(element.dataset.rowId, element);
|
||||
@@ -257,6 +410,8 @@
|
||||
element.className = "playlist-row playlist-data-row" +
|
||||
(row.isSelected ? " selected" : "");
|
||||
element.setAttribute("aria-selected", row.isSelected ? "true" : "false");
|
||||
element.draggable = !playlistMutationLocked;
|
||||
element.setAttribute("aria-disabled", playlistMutationLocked ? "true" : "false");
|
||||
[row.marketText, row.stockName, row.graphicType, row.subtype, row.pageText]
|
||||
.forEach(function (text, cellIndex) {
|
||||
const cell = element.children.item(cellIndex + 1);
|
||||
@@ -281,6 +436,59 @@
|
||||
playlistDeleteAll.disabled = interactionDisabled;
|
||||
}
|
||||
|
||||
function tabOrderFromDom() {
|
||||
return Array.from(marketTabs.querySelectorAll("button[data-tab-id]"))
|
||||
.map(function (button) { return button.dataset.tabId; });
|
||||
}
|
||||
|
||||
function clearPendingTabHoverSwap() {
|
||||
if (pendingTabHoverSwap && pendingTabHoverSwap.timeoutId !== null) {
|
||||
window.clearTimeout(pendingTabHoverSwap.timeoutId);
|
||||
}
|
||||
pendingTabHoverSwap = null;
|
||||
}
|
||||
|
||||
function acknowledgePendingTabHoverSwap(order) {
|
||||
if (!pendingTabHoverSwap) return;
|
||||
const orderKey = order.join("|");
|
||||
if (orderKey === pendingTabHoverSwap.expectedOrderKey ||
|
||||
orderKey !== pendingTabHoverSwap.initialOrderKey) {
|
||||
clearPendingTabHoverSwap();
|
||||
}
|
||||
}
|
||||
|
||||
function requestTabHoverSwap(targetId) {
|
||||
if (uiBusy || !draggedTabId || !targetId || draggedTabId === targetId ||
|
||||
pendingTabHoverSwap) return;
|
||||
|
||||
const order = tabOrderFromDom();
|
||||
const sourceIndex = order.indexOf(draggedTabId);
|
||||
const targetIndex = order.indexOf(targetId);
|
||||
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) return;
|
||||
|
||||
const expectedOrder = order.slice();
|
||||
expectedOrder[sourceIndex] = targetId;
|
||||
expectedOrder[targetIndex] = draggedTabId;
|
||||
const pending = {
|
||||
initialOrderKey: order.join("|"),
|
||||
expectedOrderKey: expectedOrder.join("|"),
|
||||
timeoutId: null
|
||||
};
|
||||
pending.timeoutId = window.setTimeout(function () {
|
||||
if (pendingTabHoverSwap === pending) {
|
||||
pendingTabHoverSwap = null;
|
||||
tabDragLastTargetId = null;
|
||||
}
|
||||
}, 500);
|
||||
pendingTabHoverSwap = pending;
|
||||
send("hover-swap-tabs", {
|
||||
source: draggedTabId,
|
||||
target: targetId,
|
||||
expectedSourceIndex: sourceIndex,
|
||||
expectedTargetIndex: targetIndex
|
||||
});
|
||||
}
|
||||
|
||||
function renderTabs(state) {
|
||||
if (!Array.isArray(state.tabs)) return;
|
||||
const existingTabs = new Map();
|
||||
@@ -298,6 +506,8 @@
|
||||
button.disabled = state.isBusy === true || state.fixedSectionBatch != null;
|
||||
button.draggable = state.isBusy !== true && state.fixedSectionBatch == null;
|
||||
});
|
||||
acknowledgePendingTabHoverSwap(
|
||||
state.tabs.map(function (tab) { return tab.id; }));
|
||||
}
|
||||
|
||||
function renderFixedCatalog(state) {
|
||||
@@ -1528,19 +1738,116 @@
|
||||
(namedPlaylistMode === "save" ? named.canSave !== true : named.canLoad !== true);
|
||||
}
|
||||
|
||||
function clearOperatorCatalogDragVisuals() {
|
||||
operatorCatalogDraftRows.querySelectorAll(".dragging, .drag-before, .drag-after")
|
||||
.forEach(function (row) {
|
||||
row.classList.remove("dragging", "drag-before", "drag-after");
|
||||
});
|
||||
}
|
||||
|
||||
function onOperatorCatalogPointerDown(event) {
|
||||
if (event.button !== 0) return;
|
||||
const rowId = event.currentTarget.dataset.operatorCatalogRowId;
|
||||
operatorCatalogDragBlockedRowId = event.target.closest("button, input")
|
||||
? rowId
|
||||
: null;
|
||||
}
|
||||
|
||||
function onOperatorCatalogPointerEnd(event) {
|
||||
if (operatorCatalogDragBlockedRowId ===
|
||||
event.currentTarget.dataset.operatorCatalogRowId) {
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onOperatorCatalogDragStart(event) {
|
||||
const row = event.currentTarget;
|
||||
const sourceRowId = row.dataset.operatorCatalogRowId;
|
||||
if (operatorCatalogMutationLocked || !sourceRowId || !event.dataTransfer ||
|
||||
operatorCatalogDragBlockedRowId === sourceRowId ||
|
||||
event.target.closest("button, input")) {
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
draggedOperatorCatalogRowId = sourceRowId;
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData(operatorCatalogDragMime, sourceRowId);
|
||||
row.classList.add("dragging");
|
||||
}
|
||||
|
||||
function onOperatorCatalogDragOver(event) {
|
||||
const target = event.currentTarget;
|
||||
if (operatorCatalogMutationLocked || !draggedOperatorCatalogRowId ||
|
||||
target.dataset.operatorCatalogRowId === draggedOperatorCatalogRowId) return;
|
||||
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
const position = dragDropPosition(event, target);
|
||||
target.classList.toggle("drag-before", position === "before");
|
||||
target.classList.toggle("drag-after", position === "after");
|
||||
}
|
||||
|
||||
function onOperatorCatalogDragLeave(event) {
|
||||
event.currentTarget.classList.remove("drag-before", "drag-after");
|
||||
}
|
||||
|
||||
function onOperatorCatalogDrop(event) {
|
||||
const target = event.currentTarget;
|
||||
const sourceRowId = draggedOperatorCatalogRowId;
|
||||
const targetRowId = target.dataset.operatorCatalogRowId;
|
||||
if (operatorCatalogMutationLocked || !sourceRowId || !targetRowId ||
|
||||
sourceRowId === targetRowId || !event.dataTransfer ||
|
||||
event.dataTransfer.getData(operatorCatalogDragMime) !== sourceRowId) {
|
||||
draggedOperatorCatalogRowId = null;
|
||||
clearOperatorCatalogDragVisuals();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const position = dragDropPosition(event, target);
|
||||
draggedOperatorCatalogRowId = null;
|
||||
clearOperatorCatalogDragVisuals();
|
||||
send("reorder-operator-catalog-draft-row", {
|
||||
sourceRowId: sourceRowId,
|
||||
targetRowId: targetRowId,
|
||||
position: position
|
||||
});
|
||||
}
|
||||
|
||||
function onOperatorCatalogDragEnd() {
|
||||
draggedOperatorCatalogRowId = null;
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
clearOperatorCatalogDragVisuals();
|
||||
}
|
||||
|
||||
function renderOperatorCatalog(state) {
|
||||
const catalog = state.operatorCatalog;
|
||||
const busy = state.isBusy === true;
|
||||
operatorCatalogMutationLocked = isOperatorMutationLocked(state);
|
||||
operatorCatalogState = catalog || null;
|
||||
if (!catalog || catalog.isOpen !== true) {
|
||||
draggedOperatorCatalogRowId = null;
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
clearOperatorCatalogDragVisuals();
|
||||
clearDelayedClick(operatorCatalogStockClickTimer);
|
||||
operatorCatalogStockClickTimer = null;
|
||||
operatorCatalogModal.hidden = true;
|
||||
operatorCatalogDraftKey = "";
|
||||
operatorCatalogNameDraft = "";
|
||||
operatorCatalogNameStatus.textContent = "";
|
||||
operatorCatalogNameStatus.className = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (operatorCatalogMutationLocked) {
|
||||
draggedOperatorCatalogRowId = null;
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
clearOperatorCatalogDragVisuals();
|
||||
}
|
||||
|
||||
operatorCatalogModal.hidden = false;
|
||||
const isTheme = catalog.entity === "theme";
|
||||
const editing = catalog.mode === "create" || catalog.mode === "edit";
|
||||
@@ -1599,6 +1906,27 @@
|
||||
operatorCatalogNameLabel.textContent = isTheme ? "테마명" : "전문가명";
|
||||
operatorCatalogName.placeholder = isTheme ? "ThemeA 이름" : "전문가 이름";
|
||||
operatorCatalogName.disabled = busy || !editing;
|
||||
const currentThemeName = operatorCatalogNameDraft.trim();
|
||||
const checkedCurrentThemeName = isTheme && currentThemeName.length > 0 &&
|
||||
catalog.checkedThemeName === currentThemeName;
|
||||
const themeNameAvailability = checkedCurrentThemeName
|
||||
? catalog.themeNameAvailability
|
||||
: "notChecked";
|
||||
operatorCatalogNameCheck.hidden = !isTheme;
|
||||
operatorCatalogNameCheck.disabled = busy || !isTheme || !editing ||
|
||||
catalog.canCheckThemeName !== true || currentThemeName.length === 0;
|
||||
operatorCatalogNameStatus.hidden = !isTheme;
|
||||
operatorCatalogNameStatus.className = themeNameAvailability === "available" ||
|
||||
themeNameAvailability === "duplicate" || themeNameAvailability === "indeterminate"
|
||||
? themeNameAvailability
|
||||
: "";
|
||||
operatorCatalogNameStatus.textContent = themeNameAvailability === "available"
|
||||
? "사용 가능"
|
||||
: themeNameAvailability === "duplicate"
|
||||
? "이미 사용 중"
|
||||
: themeNameAvailability === "indeterminate"
|
||||
? "확인 불가"
|
||||
: "";
|
||||
if (document.activeElement !== operatorCatalogStockQuery) {
|
||||
operatorCatalogStockQuery.value = catalog.stockQuery || "";
|
||||
}
|
||||
@@ -1632,6 +1960,19 @@
|
||||
(catalog.draftRows || []).forEach(function (row, index) {
|
||||
const line = document.createElement("div");
|
||||
line.className = "operator-catalog-draft-row";
|
||||
line.dataset.operatorCatalogRowId = row.rowId;
|
||||
line.draggable = !operatorCatalogMutationLocked;
|
||||
line.setAttribute(
|
||||
"aria-disabled",
|
||||
operatorCatalogMutationLocked ? "true" : "false");
|
||||
line.addEventListener("pointerdown", onOperatorCatalogPointerDown);
|
||||
line.addEventListener("pointerup", onOperatorCatalogPointerEnd);
|
||||
line.addEventListener("pointercancel", onOperatorCatalogPointerEnd);
|
||||
line.addEventListener("dragstart", onOperatorCatalogDragStart);
|
||||
line.addEventListener("dragover", onOperatorCatalogDragOver);
|
||||
line.addEventListener("dragleave", onOperatorCatalogDragLeave);
|
||||
line.addEventListener("drop", onOperatorCatalogDrop);
|
||||
line.addEventListener("dragend", onOperatorCatalogDragEnd);
|
||||
const order = document.createElement("span");
|
||||
order.textContent = String(row.position + 1);
|
||||
const name = document.createElement("strong");
|
||||
@@ -2012,6 +2353,25 @@
|
||||
});
|
||||
operatorCatalogName.addEventListener("input", function () {
|
||||
operatorCatalogNameDraft = operatorCatalogName.value;
|
||||
operatorCatalogNameStatus.textContent = "";
|
||||
operatorCatalogNameStatus.className = "";
|
||||
operatorCatalogNameCheck.disabled = uiBusy || !operatorCatalogState ||
|
||||
operatorCatalogState.entity !== "theme" ||
|
||||
(operatorCatalogState.mode !== "create" && operatorCatalogState.mode !== "edit") ||
|
||||
operatorCatalogState.canCheckThemeName !== true ||
|
||||
operatorCatalogName.value.trim().length === 0;
|
||||
});
|
||||
operatorCatalogNameCheck.addEventListener("click", function () {
|
||||
const name = operatorCatalogName.value.trim();
|
||||
if (!name) {
|
||||
operatorCatalogName.setCustomValidity("이름을 입력하세요.");
|
||||
operatorCatalogName.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
operatorCatalogName.setCustomValidity("");
|
||||
operatorCatalogNameDraft = name;
|
||||
send("check-operator-catalog-theme-name", { name: name });
|
||||
});
|
||||
function searchOperatorCatalogStocks() {
|
||||
const query = operatorCatalogStockQuery.value.trim();
|
||||
@@ -2119,12 +2479,28 @@
|
||||
searchOperatorCatalog();
|
||||
});
|
||||
|
||||
function hasDragType(dataTransfer, expectedType) {
|
||||
return !!dataTransfer && Array.from(dataTransfer.types || [])
|
||||
.some(function (type) { return type === expectedType; });
|
||||
}
|
||||
|
||||
function isSelectedCutsDrop(dataTransfer) {
|
||||
return hasDragType(dataTransfer, "text/plain") &&
|
||||
dataTransfer.getData("text/plain") === selectedCutsDragToken;
|
||||
}
|
||||
|
||||
playlistDropZone.addEventListener("dragover", function (event) {
|
||||
// A playlist row also drops inside this ancestor. Only advertise the
|
||||
// drop zone for the cut-list payload; otherwise the bubbled row drop
|
||||
// would append the still-selected cut as an extra playlist entry.
|
||||
if (!hasDragType(event.dataTransfer, "text/plain")) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
});
|
||||
playlistDropZone.addEventListener("drop", function (event) {
|
||||
if (!isSelectedCutsDrop(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
send("drop-selected-cuts", {});
|
||||
});
|
||||
playlistMoveUp.addEventListener("click", function () {
|
||||
@@ -2155,20 +2531,43 @@
|
||||
marketTabs.addEventListener("dragstart", function (event) {
|
||||
const button = event.target.closest("button[data-tab-id]");
|
||||
if (!button || !event.dataTransfer) return;
|
||||
clearPendingTabHoverSwap();
|
||||
draggedTabId = button.dataset.tabId;
|
||||
tabDragLastTargetId = null;
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/x-mbn-tab", button.dataset.tabId);
|
||||
event.dataTransfer.setData("text/x-mbn-tab", draggedTabId);
|
||||
button.classList.add("dragging");
|
||||
});
|
||||
marketTabs.addEventListener("dragover", function (event) {
|
||||
if (event.target.closest("button[data-tab-id]")) event.preventDefault();
|
||||
const target = event.target.closest("button[data-tab-id]");
|
||||
if (!target || !draggedTabId) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
const targetId = target.dataset.tabId;
|
||||
if (targetId === draggedTabId) {
|
||||
tabDragLastTargetId = null;
|
||||
return;
|
||||
}
|
||||
if (tabDragLastTargetId === targetId) return;
|
||||
if (pendingTabHoverSwap) return;
|
||||
tabDragLastTargetId = targetId;
|
||||
requestTabHoverSwap(targetId);
|
||||
});
|
||||
marketTabs.addEventListener("drop", function (event) {
|
||||
const target = event.target.closest("button[data-tab-id]");
|
||||
if (!target || !event.dataTransfer) return;
|
||||
if (!target || !event.dataTransfer || !draggedTabId) return;
|
||||
event.preventDefault();
|
||||
const source = event.dataTransfer.getData("text/x-mbn-tab");
|
||||
if (source && source !== target.dataset.tabId) {
|
||||
send("swap-tabs", { source: source, target: target.dataset.tabId });
|
||||
}
|
||||
if (event.dataTransfer.getData("text/x-mbn-tab") !== draggedTabId) return;
|
||||
draggedTabId = null;
|
||||
tabDragLastTargetId = null;
|
||||
marketTabs.querySelectorAll("button.dragging")
|
||||
.forEach(function (button) { button.classList.remove("dragging"); });
|
||||
});
|
||||
marketTabs.addEventListener("dragend", function () {
|
||||
draggedTabId = null;
|
||||
tabDragLastTargetId = null;
|
||||
marketTabs.querySelectorAll("button.dragging")
|
||||
.forEach(function (button) { button.classList.remove("dragging"); });
|
||||
});
|
||||
function sendMovingAverages() {
|
||||
send("set-moving-averages", {
|
||||
@@ -2498,6 +2897,10 @@
|
||||
send("search-trading-halts", { query: event.target.value });
|
||||
}
|
||||
});
|
||||
window.addEventListener("pointerup", function () {
|
||||
deferredPlaylistPlainSelectionRowId = null;
|
||||
operatorCatalogDragBlockedRowId = null;
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Delete" && !event.repeat && dialog.hidden && manualModal.hidden &&
|
||||
manualListModal.hidden && namedPlaylistModal.hidden && operatorCatalogModal.hidden) {
|
||||
|
||||
@@ -196,10 +196,12 @@
|
||||
<h3 id="operator-catalog-editor-title">추가·편집</h3>
|
||||
<span id="operator-catalog-code" class="operator-catalog-code"></span>
|
||||
</div>
|
||||
<label class="operator-catalog-name-field">
|
||||
<span id="operator-catalog-name-label">이름</span>
|
||||
<div class="operator-catalog-name-field">
|
||||
<label id="operator-catalog-name-label" for="operator-catalog-name">이름</label>
|
||||
<input id="operator-catalog-name" maxlength="128" autocomplete="off">
|
||||
</label>
|
||||
<button id="operator-catalog-name-check" type="button">중복확인</button>
|
||||
<small id="operator-catalog-name-status" aria-live="polite"></small>
|
||||
</div>
|
||||
<div class="operator-catalog-stock-search">
|
||||
<input id="operator-catalog-stock-query" type="search" maxlength="64"
|
||||
autocomplete="off" placeholder="종목명 검색">
|
||||
|
||||
@@ -140,6 +140,8 @@ button:disabled { color: #555; opacity: 1; }
|
||||
.market-tabs { display: flex; height: 29px; border-bottom: 1px solid #999; }
|
||||
.market-tabs button { min-width: 48px; padding: 3px 8px; border-width: 0 1px 0 0; background: #efefef; }
|
||||
.market-tabs button.active { background: #fff; border-top: 2px solid #e8781d; }
|
||||
.market-tabs button[draggable="true"] { cursor: grab; }
|
||||
.market-tabs button.dragging { opacity: .58; cursor: grabbing; }
|
||||
.expand-line { height: 31px; padding: 7px 8px 0; }
|
||||
.category-title { height: 27px; border-bottom: 1px solid #777; font-size: 12px; }
|
||||
.category-tree { flex: 1; overflow: auto; padding: 7px 12px; background: #fff; border-left: 1px solid #aaa; border-right: 1px solid #aaa; }
|
||||
@@ -237,7 +239,10 @@ button:disabled { color: #555; opacity: 1; }
|
||||
.operator-catalog-results button.selected { background: #dcebdc; }
|
||||
.operator-catalog-editor { grid-template-rows: auto auto auto minmax(92px, 20%) auto auto minmax(120px, 1fr) auto; gap: 7px; padding-bottom: 8px; }
|
||||
.operator-catalog-editor[hidden] { display: none; }
|
||||
.operator-catalog-name-field { display: grid; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 6px; padding: 0 9px; }
|
||||
.operator-catalog-name-field { display: grid; grid-template-columns: 76px minmax(0, 1fr) auto; align-items: center; gap: 6px; padding: 0 9px; }
|
||||
.operator-catalog-name-field small { grid-column: 2 / 4; min-height: 16px; color: #555; }
|
||||
.operator-catalog-name-field small.available { color: #176b2d; font-weight: 600; }
|
||||
.operator-catalog-name-field small.duplicate, .operator-catalog-name-field small.indeterminate { color: #a40000; font-weight: 600; }
|
||||
.operator-catalog-stock-search { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; padding: 0 9px; }
|
||||
.operator-catalog-stock-results { margin: 0 9px; border: 1px solid #aaa; }
|
||||
.operator-catalog-stock-results button { display: grid; grid-template-columns: minmax(0, 1fr) auto; width: 100%; padding: 4px 7px; border: 0; border-bottom: 1px solid #ddd; background: #fff; text-align: left; }
|
||||
@@ -252,6 +257,10 @@ button:disabled { color: #555; opacity: 1; }
|
||||
.operator-catalog-draft-row > span { text-align: center; }
|
||||
.operator-catalog-draft-row > small { color: #555; text-align: right; }
|
||||
.operator-catalog-draft-row > input { min-width: 90px; width: 100%; height: 27px; text-align: right; }
|
||||
.operator-catalog-draft-row[draggable="true"] { cursor: grab; }
|
||||
.operator-catalog-draft-row.dragging { opacity: .55; cursor: grabbing; }
|
||||
.operator-catalog-draft-row.drag-before { box-shadow: inset 0 3px #176b2d; }
|
||||
.operator-catalog-draft-row.drag-after { box-shadow: inset 0 -3px #176b2d; }
|
||||
.operator-catalog-editor-actions { display: grid; grid-template-columns: auto 1fr auto auto; gap: 7px; padding: 2px 9px 0; }
|
||||
.operator-catalog-editor-actions button:last-child { min-width: 92px; color: #fff; background: #176b2d; font-weight: 700; }
|
||||
|
||||
@@ -272,6 +281,10 @@ button:disabled { color: #555; opacity: 1; }
|
||||
.playlist-data-row .enabled-cell { text-align: center; }
|
||||
.playlist-data-row.selected { background: #dcdcdc; }
|
||||
.playlist-data-row:focus { outline: 1px dotted #333; outline-offset: -2px; }
|
||||
.playlist-data-row[draggable="true"] { cursor: grab; }
|
||||
.playlist-data-row.dragging { opacity: .55; cursor: grabbing; }
|
||||
.playlist-data-row.drag-before { box-shadow: inset 0 3px #176b2d; }
|
||||
.playlist-data-row.drag-after { box-shadow: inset 0 -3px #176b2d; }
|
||||
|
||||
.playout-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: center; gap: 12px; }
|
||||
.playout-actions button { height: 51px; background: #fff; font-size: 25px; font-weight: 700; }
|
||||
|
||||
Reference in New Issue
Block a user