|
@@ -356,40 +356,6 @@
|
|
|
return out;
|
|
return out;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Render the inline reference icons (one per linked task) into `host`.
|
|
|
|
|
- // Each link gets a small arrow icon whose hover title carries the
|
|
|
|
|
- // "Copied from / Copied to: <title> (<sprint>)" detail. The icon
|
|
|
|
|
- // anchors to the linked sprint page.
|
|
|
|
|
- function renderTaskRefs(host, links) {
|
|
|
|
|
- host.innerHTML = '';
|
|
|
|
|
- if (!Array.isArray(links) || links.length === 0) {
|
|
|
|
|
- host.classList.add('hidden');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- host.classList.remove('hidden');
|
|
|
|
|
- links.forEach(function (l) {
|
|
|
|
|
- if (!l || typeof l !== 'object') { return; }
|
|
|
|
|
- const dir = l.direction === 'source' ? 'source' : 'copy';
|
|
|
|
|
- const a = document.createElement('a');
|
|
|
|
|
- a.href = '/sprints/' + l.sprint_id;
|
|
|
|
|
- a.className = 'task-ref-icon inline-flex items-center justify-center w-5 h-5 rounded text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700';
|
|
|
|
|
- const label = (dir === 'source' ? 'Copied from' : 'Copied to')
|
|
|
|
|
- + ': ' + (l.title || '') + ' (' + (l.sprint_name || '') + ')';
|
|
|
|
|
- a.title = label;
|
|
|
|
|
- a.setAttribute('aria-label', label);
|
|
|
|
|
- const svg = dir === 'source'
|
|
|
|
|
- ? '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">'
|
|
|
|
|
- + '<path d="M10 4L4 8l6 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
|
|
|
- + '<line x1="4" y1="8" x2="13" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>'
|
|
|
|
|
- + '</svg>'
|
|
|
|
|
- : '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">'
|
|
|
|
|
- + '<path d="M6 4l6 4-6 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
|
|
|
- + '<line x1="3" y1="8" x2="12" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>'
|
|
|
|
|
- + '</svg>';
|
|
|
|
|
- a.innerHTML = svg;
|
|
|
|
|
- host.appendChild(a);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
// Build a task <tr> from an object — vanilla JS DOM construction.
|
|
// Build a task <tr> from an object — vanilla JS DOM construction.
|
|
|
function buildTaskRow(task, assignments) {
|
|
function buildTaskRow(task, assignments) {
|
|
@@ -424,21 +390,19 @@
|
|
|
tdMenu.appendChild(trig);
|
|
tdMenu.appendChild(trig);
|
|
|
tr.appendChild(tdMenu);
|
|
tr.appendChild(tdMenu);
|
|
|
|
|
|
|
|
- // title cell — grid layout: [title input] [URL + ref icons] [info icon]
|
|
|
|
|
|
|
+ // title cell — title input + small "open URL" anchor; description /
|
|
|
|
|
+ // refs / actions all live in the hamburger popup.
|
|
|
const tdTitle = document.createElement('td');
|
|
const tdTitle = document.createElement('td');
|
|
|
tdTitle.className = 'px-2 py-1 min-w-[14rem]';
|
|
tdTitle.className = 'px-2 py-1 min-w-[14rem]';
|
|
|
const titleWrap = document.createElement('div');
|
|
const titleWrap = document.createElement('div');
|
|
|
- titleWrap.className = 'task-title-grid';
|
|
|
|
|
|
|
+ titleWrap.className = 'flex items-center gap-1.5';
|
|
|
const title = document.createElement('input');
|
|
const title = document.createElement('input');
|
|
|
title.type = 'text';
|
|
title.type = 'text';
|
|
|
title.setAttribute('data-title', '');
|
|
title.setAttribute('data-title', '');
|
|
|
title.value = task.title || '';
|
|
title.value = task.title || '';
|
|
|
- title.className = 'rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500 min-w-0';
|
|
|
|
|
|
|
+ title.className = 'flex-1 rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500';
|
|
|
titleWrap.appendChild(title);
|
|
titleWrap.appendChild(title);
|
|
|
|
|
|
|
|
- const midSlot = document.createElement('div');
|
|
|
|
|
- midSlot.className = 'task-title-mid';
|
|
|
|
|
-
|
|
|
|
|
const urlLink = document.createElement('a');
|
|
const urlLink = document.createElement('a');
|
|
|
urlLink.setAttribute('data-task-url-link', '');
|
|
urlLink.setAttribute('data-task-url-link', '');
|
|
|
urlLink.target = '_blank';
|
|
urlLink.target = '_blank';
|
|
@@ -453,32 +417,7 @@
|
|
|
+ '<path d="M14 2L7 9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
+ '<path d="M14 2L7 9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
|
+ '<path d="M12 9v4a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
+ '<path d="M12 9v4a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
|
+ '</svg>';
|
|
+ '</svg>';
|
|
|
- midSlot.appendChild(urlLink);
|
|
|
|
|
-
|
|
|
|
|
- const refsWrap = document.createElement('span');
|
|
|
|
|
- refsWrap.setAttribute('data-task-refs', '');
|
|
|
|
|
- refsWrap.className = 'task-refs inline-flex items-center gap-0.5';
|
|
|
|
|
- renderTaskRefs(refsWrap, Array.isArray(task.links) ? task.links : []);
|
|
|
|
|
- midSlot.appendChild(refsWrap);
|
|
|
|
|
-
|
|
|
|
|
- titleWrap.appendChild(midSlot);
|
|
|
|
|
-
|
|
|
|
|
- const infoBtn = document.createElement('button');
|
|
|
|
|
- infoBtn.type = 'button';
|
|
|
|
|
- infoBtn.setAttribute('data-task-info-trigger', '');
|
|
|
|
|
- infoBtn.className = 'task-info-trigger inline-flex items-center justify-center w-5 h-5 rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:text-slate-500 dark:hover:bg-slate-700 dark:hover:text-slate-200';
|
|
|
|
|
- infoBtn.title = 'Task info';
|
|
|
|
|
- infoBtn.setAttribute('aria-label', 'Task info');
|
|
|
|
|
- infoBtn.setAttribute('aria-haspopup', 'true');
|
|
|
|
|
- infoBtn.setAttribute('aria-expanded', 'false');
|
|
|
|
|
- const hasInfo = !!(task.description || task.url || (Array.isArray(task.links) && task.links.length > 0));
|
|
|
|
|
- if (!hasInfo) { infoBtn.classList.add('hidden'); }
|
|
|
|
|
- infoBtn.innerHTML = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">'
|
|
|
|
|
- + '<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4"/>'
|
|
|
|
|
- + '<circle cx="8" cy="4.6" r="0.85" fill="currentColor"/>'
|
|
|
|
|
- + '<line x1="8" y1="7" x2="8" y2="11.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'
|
|
|
|
|
- + '</svg>';
|
|
|
|
|
- titleWrap.appendChild(infoBtn);
|
|
|
|
|
|
|
+ titleWrap.appendChild(urlLink);
|
|
|
|
|
|
|
|
tdTitle.appendChild(titleWrap);
|
|
tdTitle.appendChild(titleWrap);
|
|
|
tr.appendChild(tdTitle);
|
|
tr.appendChild(tdTitle);
|
|
@@ -629,7 +568,9 @@
|
|
|
const tr = qs('tr[data-task-id="' + taskId + '"]', taskTbody);
|
|
const tr = qs('tr[data-task-id="' + taskId + '"]', taskTbody);
|
|
|
if (!tr) { return; }
|
|
if (!tr) { return; }
|
|
|
const titleInp = qs('[data-title]', tr);
|
|
const titleInp = qs('[data-title]', tr);
|
|
|
- const title = titleInp ? titleInp.value : '(untitled)';
|
|
|
|
|
|
|
+ const title = titleInp
|
|
|
|
|
+ ? String(titleInp.value || '')
|
|
|
|
|
+ : String(tr.getAttribute('data-task-title') || '(untitled)');
|
|
|
if (!window.confirm('Delete task "' + title + '"?')) { return; }
|
|
if (!window.confirm('Delete task "' + title + '"?')) { return; }
|
|
|
request('DELETE', '/tasks/' + taskId)
|
|
request('DELETE', '/tasks/' + taskId)
|
|
|
.then(function (data) {
|
|
.then(function (data) {
|
|
@@ -946,8 +887,21 @@
|
|
|
|
|
|
|
|
// --- Task hamburger menu + details modal + click-pickup reorder -----
|
|
// --- Task hamburger menu + details modal + click-pickup reorder -----
|
|
|
//
|
|
//
|
|
|
- // Replaces SortableJS for tasks. The menu, modal, description popover,
|
|
|
|
|
- // and pickup overlay are all single body-attached nodes built lazily.
|
|
|
|
|
|
|
+ // Single body-attached popup. Opens to the right of the trigger,
|
|
|
|
|
+ // vertically centred on it (cellPopover positioning model). Three
|
|
|
|
|
+ // panes:
|
|
|
|
|
+ // - Left: vertical menu (Edit task / Move task to sprint /
|
|
|
|
|
+ // Copy task to sprint / Move up/down / divider /
|
|
|
|
|
+ // Delete task).
|
|
|
|
|
+ // - Right: read-only task info — title, description, URL link,
|
|
|
|
|
+ // and reference chips at the bottom.
|
|
|
|
|
+ // - Flyout (third column, hidden by default): sprint chooser
|
|
|
|
|
+ // for the Move / Copy actions.
|
|
|
|
|
+ // Closing mirrors the cell popover: outside-pointerdown, Escape,
|
|
|
|
|
+ // scroll / resize, and a 250 ms mouseleave grace. The popup is
|
|
|
|
|
+ // shown to non-admins too — the corresponding TaskController
|
|
|
|
|
+ // endpoints (update / delete / move / copy / reorder) accept any
|
|
|
|
|
+ // signed-in user.
|
|
|
|
|
|
|
|
const sprintChoices = (function () {
|
|
const sprintChoices = (function () {
|
|
|
if (!taskSection) { return []; }
|
|
if (!taskSection) { return []; }
|
|
@@ -961,16 +915,38 @@
|
|
|
} catch (_) { return []; }
|
|
} catch (_) { return []; }
|
|
|
})();
|
|
})();
|
|
|
|
|
|
|
|
- let taskMenu = null; // root container
|
|
|
|
|
- let taskMenuTaskId = null;
|
|
|
|
|
- let taskMenuTrigger = null; // the button it was opened from
|
|
|
|
|
- let taskMenuOpenAt = 0;
|
|
|
|
|
|
|
+ let taskMenu = null; // root container (popup)
|
|
|
|
|
+ let taskMenuFlyout = null; // third-column sprint chooser
|
|
|
|
|
+ let taskMenuListEl = null; // left vertical menu
|
|
|
|
|
+ let taskMenuInfoEl = null; // right info pane
|
|
|
|
|
+ let taskMenuTaskId = null;
|
|
|
|
|
+ let taskMenuTrigger = null; // the hamburger button it was opened from
|
|
|
|
|
+ let taskMenuAction = null; // 'move' | 'copy' when flyout is open
|
|
|
|
|
+ let taskMenuOpenAt = 0;
|
|
|
|
|
+ let taskMenuGrace = null;
|
|
|
|
|
+
|
|
|
|
|
+ function cancelTaskMenuGrace() {
|
|
|
|
|
+ if (taskMenuGrace) { clearTimeout(taskMenuGrace); taskMenuGrace = null; }
|
|
|
|
|
+ }
|
|
|
|
|
+ function scheduleTaskMenuGrace() {
|
|
|
|
|
+ cancelTaskMenuGrace();
|
|
|
|
|
+ taskMenuGrace = setTimeout(closeTaskMenu, 250);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
function buildTaskMenu() {
|
|
function buildTaskMenu() {
|
|
|
if (taskMenu) { return taskMenu; }
|
|
if (taskMenu) { return taskMenu; }
|
|
|
const r = document.createElement('div');
|
|
const r = document.createElement('div');
|
|
|
r.className = 'task-menu hidden';
|
|
r.className = 'task-menu hidden';
|
|
|
- r.setAttribute('role', 'menu');
|
|
|
|
|
|
|
+ r.setAttribute('role', 'dialog');
|
|
|
|
|
+ r.setAttribute('aria-label', 'Task actions');
|
|
|
|
|
+
|
|
|
|
|
+ const inner = document.createElement('div');
|
|
|
|
|
+ inner.className = 'task-menu-inner';
|
|
|
|
|
+
|
|
|
|
|
+ // Left column: vertical menu list.
|
|
|
|
|
+ const list = document.createElement('div');
|
|
|
|
|
+ list.className = 'task-menu-list';
|
|
|
|
|
+ list.setAttribute('role', 'menu');
|
|
|
|
|
|
|
|
function item(label, action, opts) {
|
|
function item(label, action, opts) {
|
|
|
const b = document.createElement('button');
|
|
const b = document.createElement('button');
|
|
@@ -979,70 +955,171 @@
|
|
|
b.setAttribute('data-task-menu-item', action);
|
|
b.setAttribute('data-task-menu-item', action);
|
|
|
b.className = 'task-menu-item';
|
|
b.className = 'task-menu-item';
|
|
|
if (opts && opts.danger) { b.classList.add('task-menu-item-danger'); }
|
|
if (opts && opts.danger) { b.classList.add('task-menu-item-danger'); }
|
|
|
- b.textContent = label;
|
|
|
|
|
|
|
+ if (opts && opts.withSub) {
|
|
|
|
|
+ b.classList.add('task-menu-item-with-sub');
|
|
|
|
|
+ b.setAttribute('data-task-submenu-trigger', action);
|
|
|
|
|
+ b.innerHTML = '<span>' + label + '</span><span class="opacity-60">▸</span>';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ b.textContent = label;
|
|
|
|
|
+ }
|
|
|
return b;
|
|
return b;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function submenu(label, action) {
|
|
|
|
|
- const wrap = document.createElement('div');
|
|
|
|
|
- wrap.className = 'task-menu-subwrap';
|
|
|
|
|
- wrap.setAttribute('data-task-submenu', action);
|
|
|
|
|
- const b = document.createElement('button');
|
|
|
|
|
- b.type = 'button';
|
|
|
|
|
- b.setAttribute('role', 'menuitem');
|
|
|
|
|
- b.setAttribute('data-task-submenu-trigger', action);
|
|
|
|
|
- b.className = 'task-menu-item task-menu-item-with-sub';
|
|
|
|
|
- b.innerHTML = '<span>' + label + '</span><span class="opacity-60">▸</span>';
|
|
|
|
|
- wrap.appendChild(b);
|
|
|
|
|
-
|
|
|
|
|
- const list = document.createElement('div');
|
|
|
|
|
- list.className = 'task-menu-sub hidden';
|
|
|
|
|
- list.setAttribute('data-task-submenu-list', action);
|
|
|
|
|
- wrap.appendChild(list);
|
|
|
|
|
- return wrap;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ list.appendChild(item('Edit task', 'edit'));
|
|
|
|
|
+ list.appendChild(item('Move task to sprint', 'move', { withSub: true }));
|
|
|
|
|
+ list.appendChild(item('Copy task to sprint', 'copy', { withSub: true }));
|
|
|
|
|
+ list.appendChild(item('Move up/down', 'pickup'));
|
|
|
|
|
+ const divider = document.createElement('div');
|
|
|
|
|
+ divider.className = 'task-menu-divider';
|
|
|
|
|
+ list.appendChild(divider);
|
|
|
|
|
+ list.appendChild(item('Delete task', 'delete', { danger: true }));
|
|
|
|
|
+ inner.appendChild(list);
|
|
|
|
|
+ taskMenuListEl = list;
|
|
|
|
|
+
|
|
|
|
|
+ // Right column: read-only task info pane.
|
|
|
|
|
+ const info = document.createElement('div');
|
|
|
|
|
+ info.className = 'task-menu-info';
|
|
|
|
|
+ info.innerHTML =
|
|
|
|
|
+ '<div class="task-menu-info-title" data-info-title></div>'
|
|
|
|
|
+ + '<div class="task-menu-info-desc" data-info-desc></div>'
|
|
|
|
|
+ + '<div class="task-menu-info-url hidden" data-info-url-wrap><a class="task-menu-info-url-link" data-info-url target="_blank" rel="noopener noreferrer"></a></div>'
|
|
|
|
|
+ + '<div class="task-menu-info-refs hidden" data-info-refs-wrap>'
|
|
|
|
|
+ + '<div class="task-menu-info-refs-head">References</div>'
|
|
|
|
|
+ + '<div class="task-menu-info-refs-list" data-info-refs></div>'
|
|
|
|
|
+ + '</div>';
|
|
|
|
|
+ inner.appendChild(info);
|
|
|
|
|
+ taskMenuInfoEl = info;
|
|
|
|
|
+
|
|
|
|
|
+ // Third column (flyout): hidden until "Move" or "Copy" picked.
|
|
|
|
|
+ const flyout = document.createElement('div');
|
|
|
|
|
+ flyout.className = 'task-menu-flyout hidden';
|
|
|
|
|
+ flyout.innerHTML =
|
|
|
|
|
+ '<div class="task-menu-flyout-head" data-flyout-head></div>'
|
|
|
|
|
+ + '<div class="task-menu-flyout-list" data-task-submenu-list></div>';
|
|
|
|
|
+ inner.appendChild(flyout);
|
|
|
|
|
+ taskMenuFlyout = flyout;
|
|
|
|
|
+
|
|
|
|
|
+ r.appendChild(inner);
|
|
|
|
|
+ document.body.appendChild(r);
|
|
|
|
|
|
|
|
- r.appendChild(item('Edit details…', 'edit'));
|
|
|
|
|
- r.appendChild(submenu('Move to sprint', 'move'));
|
|
|
|
|
- r.appendChild(submenu('Copy to sprint', 'copy'));
|
|
|
|
|
- r.appendChild(item('Move (pick up)', 'pickup'));
|
|
|
|
|
- r.appendChild(item('Delete', 'delete', { danger: true }));
|
|
|
|
|
|
|
+ // Mouseleave-grace identical to cellPopover.
|
|
|
|
|
+ r.addEventListener('mouseenter', cancelTaskMenuGrace);
|
|
|
|
|
+ r.addEventListener('mouseleave', scheduleTaskMenuGrace);
|
|
|
|
|
|
|
|
- document.body.appendChild(r);
|
|
|
|
|
taskMenu = r;
|
|
taskMenu = r;
|
|
|
return r;
|
|
return r;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function refreshTaskMenuChoices(currentSprintId) {
|
|
|
|
|
- const moveList = taskMenu.querySelector('[data-task-submenu-list="move"]');
|
|
|
|
|
- const copyList = taskMenu.querySelector('[data-task-submenu-list="copy"]');
|
|
|
|
|
- [moveList, copyList].forEach((el) => { if (el) { el.innerHTML = ''; } });
|
|
|
|
|
- if (sprintChoices.length === 0) {
|
|
|
|
|
- [moveList, copyList].forEach(function (el) {
|
|
|
|
|
- if (!el) { return; }
|
|
|
|
|
- const empty = document.createElement('div');
|
|
|
|
|
- empty.className = 'task-menu-sub-empty';
|
|
|
|
|
- empty.textContent = 'No other sprints';
|
|
|
|
|
- el.appendChild(empty);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ function fillTaskMenuFlyout(action, currentSprintId) {
|
|
|
|
|
+ if (!taskMenuFlyout) { return; }
|
|
|
|
|
+ const headEl = taskMenuFlyout.querySelector('[data-flyout-head]');
|
|
|
|
|
+ const listEl = taskMenuFlyout.querySelector('[data-task-submenu-list]');
|
|
|
|
|
+ if (!headEl || !listEl) { return; }
|
|
|
|
|
+
|
|
|
|
|
+ listEl.setAttribute('data-task-submenu-list', action);
|
|
|
|
|
+ headEl.textContent = action === 'move' ? 'Move to sprint' : 'Copy to sprint';
|
|
|
|
|
+ listEl.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ const others = sprintChoices.filter((s) => Number(s.id) !== Number(currentSprintId));
|
|
|
|
|
+ if (others.length === 0) {
|
|
|
|
|
+ const empty = document.createElement('div');
|
|
|
|
|
+ empty.className = 'task-menu-sub-empty';
|
|
|
|
|
+ empty.textContent = 'No other sprints';
|
|
|
|
|
+ listEl.appendChild(empty);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- sprintChoices.forEach(function (s) {
|
|
|
|
|
- if (Number(s.id) === Number(currentSprintId)) { return; }
|
|
|
|
|
- ['move', 'copy'].forEach(function (action) {
|
|
|
|
|
- const list = action === 'move' ? moveList : copyList;
|
|
|
|
|
- if (!list) { return; }
|
|
|
|
|
- const b = document.createElement('button');
|
|
|
|
|
- b.type = 'button';
|
|
|
|
|
- b.setAttribute('role', 'menuitem');
|
|
|
|
|
- b.setAttribute('data-task-' + action + '-target', String(s.id));
|
|
|
|
|
- b.className = 'task-menu-sub-item';
|
|
|
|
|
- b.textContent = s.name;
|
|
|
|
|
- list.appendChild(b);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ others.forEach(function (s) {
|
|
|
|
|
+ const b = document.createElement('button');
|
|
|
|
|
+ b.type = 'button';
|
|
|
|
|
+ b.setAttribute('role', 'menuitem');
|
|
|
|
|
+ b.setAttribute('data-task-' + action + '-target', String(s.id));
|
|
|
|
|
+ b.className = 'task-menu-sub-item';
|
|
|
|
|
+ b.textContent = s.name;
|
|
|
|
|
+ listEl.appendChild(b);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function setFlyoutActiveTrigger(action) {
|
|
|
|
|
+ if (!taskMenuListEl) { return; }
|
|
|
|
|
+ qsa('[data-task-submenu-trigger]', taskMenuListEl).forEach(function (b) {
|
|
|
|
|
+ b.classList.toggle('task-menu-item-active',
|
|
|
|
|
+ b.getAttribute('data-task-submenu-trigger') === action);
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function openTaskMenuFlyout(action) {
|
|
|
|
|
+ if (!taskMenu) { return; }
|
|
|
|
|
+ taskMenuAction = action;
|
|
|
|
|
+ fillTaskMenuFlyout(action, sprintId);
|
|
|
|
|
+ setFlyoutActiveTrigger(action);
|
|
|
|
|
+ taskMenuFlyout.classList.remove('hidden');
|
|
|
|
|
+ positionTaskMenu();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeTaskMenuFlyout() {
|
|
|
|
|
+ if (taskMenuFlyout) { taskMenuFlyout.classList.add('hidden'); }
|
|
|
|
|
+ setFlyoutActiveTrigger(null);
|
|
|
|
|
+ taskMenuAction = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function fillTaskMenuInfo(tr) {
|
|
|
|
|
+ if (!taskMenuInfoEl) { return; }
|
|
|
|
|
+ const titleEl = taskMenuInfoEl.querySelector('[data-info-title]');
|
|
|
|
|
+ const descEl = taskMenuInfoEl.querySelector('[data-info-desc]');
|
|
|
|
|
+ const urlWrap = taskMenuInfoEl.querySelector('[data-info-url-wrap]');
|
|
|
|
|
+ const urlLink = taskMenuInfoEl.querySelector('[data-info-url]');
|
|
|
|
|
+ const refsWrap = taskMenuInfoEl.querySelector('[data-info-refs-wrap]');
|
|
|
|
|
+ const refsList = taskMenuInfoEl.querySelector('[data-info-refs]');
|
|
|
|
|
+
|
|
|
|
|
+ const titleAttr = tr.getAttribute('data-task-title') || '';
|
|
|
|
|
+ const titleInp = qs('input[data-title]', tr);
|
|
|
|
|
+ const liveTitle = titleInp ? String(titleInp.value || '') : titleAttr;
|
|
|
|
|
+ const desc = tr.getAttribute('data-description') || '';
|
|
|
|
|
+ const url = tr.getAttribute('data-url') || '';
|
|
|
|
|
+ let links = [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed = JSON.parse(tr.getAttribute('data-links') || '[]');
|
|
|
|
|
+ if (Array.isArray(parsed)) { links = parsed; }
|
|
|
|
|
+ } catch (_) { /* ignore */ }
|
|
|
|
|
+
|
|
|
|
|
+ if (titleEl) { titleEl.textContent = liveTitle; }
|
|
|
|
|
+ if (descEl) {
|
|
|
|
|
+ descEl.textContent = desc;
|
|
|
|
|
+ descEl.classList.toggle('hidden', desc === '');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (urlWrap && urlLink) {
|
|
|
|
|
+ if (url) {
|
|
|
|
|
+ urlLink.href = url;
|
|
|
|
|
+ urlLink.textContent = url;
|
|
|
|
|
+ urlWrap.classList.remove('hidden');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ urlLink.removeAttribute('href');
|
|
|
|
|
+ urlLink.textContent = '';
|
|
|
|
|
+ urlWrap.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (refsWrap && refsList) {
|
|
|
|
|
+ refsList.innerHTML = '';
|
|
|
|
|
+ if (links.length > 0) {
|
|
|
|
|
+ refsWrap.classList.remove('hidden');
|
|
|
|
|
+ links.forEach(function (l) {
|
|
|
|
|
+ if (!l || typeof l !== 'object') { return; }
|
|
|
|
|
+ const dirLabel = l.direction === 'source' ? 'Copied from' : 'Copied to';
|
|
|
|
|
+ const arrow = l.direction === 'source' ? '←' : '→';
|
|
|
|
|
+ const a = document.createElement('a');
|
|
|
|
|
+ a.href = '/sprints/' + l.sprint_id;
|
|
|
|
|
+ a.className = 'task-menu-info-ref-chip';
|
|
|
|
|
+ a.title = dirLabel + ': ' + (l.title || '') + ' (' + (l.sprint_name || '') + ')';
|
|
|
|
|
+ a.innerHTML = '<span class="opacity-60">' + arrow + '</span> '
|
|
|
|
|
+ + '<span>' + (l.sprint_name ? String(l.sprint_name) : '') + '</span>';
|
|
|
|
|
+ refsList.appendChild(a);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ refsWrap.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function positionTaskMenu() {
|
|
function positionTaskMenu() {
|
|
|
if (!taskMenu || !taskMenuTrigger) { return; }
|
|
if (!taskMenu || !taskMenuTrigger) { return; }
|
|
|
const rect = taskMenuTrigger.getBoundingClientRect();
|
|
const rect = taskMenuTrigger.getBoundingClientRect();
|
|
@@ -1051,14 +1128,15 @@
|
|
|
const vw = document.documentElement.clientWidth;
|
|
const vw = document.documentElement.clientWidth;
|
|
|
const vh = document.documentElement.clientHeight;
|
|
const vh = document.documentElement.clientHeight;
|
|
|
|
|
|
|
|
- let top = window.scrollY + rect.bottom + 4;
|
|
|
|
|
- let left = window.scrollX + rect.left;
|
|
|
|
|
|
|
+ let top = window.scrollY + rect.top + rect.height / 2 - mh / 2;
|
|
|
|
|
+ let left = window.scrollX + rect.right + 8;
|
|
|
if (left + mw > window.scrollX + vw - 8) {
|
|
if (left + mw > window.scrollX + vw - 8) {
|
|
|
- left = window.scrollX + vw - mw - 8;
|
|
|
|
|
|
|
+ left = window.scrollX + rect.left - mw - 8;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (left < window.scrollX + 8) { left = window.scrollX + 8; }
|
|
|
|
|
+ if (top < window.scrollY + 8) { top = window.scrollY + 8; }
|
|
|
if (top + mh > window.scrollY + vh - 8) {
|
|
if (top + mh > window.scrollY + vh - 8) {
|
|
|
- top = window.scrollY + rect.top - mh - 4;
|
|
|
|
|
- if (top < window.scrollY + 8) { top = window.scrollY + 8; }
|
|
|
|
|
|
|
+ top = window.scrollY + vh - mh - 8;
|
|
|
}
|
|
}
|
|
|
taskMenu.style.top = top + 'px';
|
|
taskMenu.style.top = top + 'px';
|
|
|
taskMenu.style.left = left + 'px';
|
|
taskMenu.style.left = left + 'px';
|
|
@@ -1072,27 +1150,22 @@
|
|
|
if (!Number.isFinite(taskId)) { return; }
|
|
if (!Number.isFinite(taskId)) { return; }
|
|
|
|
|
|
|
|
buildTaskMenu();
|
|
buildTaskMenu();
|
|
|
- refreshTaskMenuChoices(sprintId);
|
|
|
|
|
- closeAllTaskSubmenus();
|
|
|
|
|
|
|
+ closeTaskMenuFlyout();
|
|
|
|
|
+ fillTaskMenuInfo(tr);
|
|
|
taskMenuTaskId = taskId;
|
|
taskMenuTaskId = taskId;
|
|
|
taskMenuTrigger = trigger;
|
|
taskMenuTrigger = trigger;
|
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|
|
|
|
+ cancelTaskMenuGrace();
|
|
|
taskMenu.classList.remove('hidden');
|
|
taskMenu.classList.remove('hidden');
|
|
|
positionTaskMenu();
|
|
positionTaskMenu();
|
|
|
taskMenuOpenAt = Date.now();
|
|
taskMenuOpenAt = Date.now();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function closeAllTaskSubmenus() {
|
|
|
|
|
- if (!taskMenu) { return; }
|
|
|
|
|
- qsa('[data-task-submenu-list]', taskMenu).forEach(function (el) {
|
|
|
|
|
- el.classList.add('hidden');
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
function closeTaskMenu() {
|
|
function closeTaskMenu() {
|
|
|
|
|
+ cancelTaskMenuGrace();
|
|
|
if (taskMenu) { taskMenu.classList.add('hidden'); }
|
|
if (taskMenu) { taskMenu.classList.add('hidden'); }
|
|
|
if (taskMenuTrigger) { taskMenuTrigger.setAttribute('aria-expanded', 'false'); }
|
|
if (taskMenuTrigger) { taskMenuTrigger.setAttribute('aria-expanded', 'false'); }
|
|
|
- closeAllTaskSubmenus();
|
|
|
|
|
|
|
+ closeTaskMenuFlyout();
|
|
|
taskMenuTaskId = null;
|
|
taskMenuTaskId = null;
|
|
|
taskMenuTrigger = null;
|
|
taskMenuTrigger = null;
|
|
|
}
|
|
}
|
|
@@ -1121,6 +1194,7 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Any scroll / resize closes — don't try to follow a moving anchor.
|
|
|
window.addEventListener('scroll', function () {
|
|
window.addEventListener('scroll', function () {
|
|
|
if (taskMenu && !taskMenu.classList.contains('hidden')) { closeTaskMenu(); }
|
|
if (taskMenu && !taskMenu.classList.contains('hidden')) { closeTaskMenu(); }
|
|
|
}, true);
|
|
}, true);
|
|
@@ -1135,10 +1209,11 @@
|
|
|
if (subTrig && taskMenu.contains(subTrig)) {
|
|
if (subTrig && taskMenu.contains(subTrig)) {
|
|
|
ev.preventDefault();
|
|
ev.preventDefault();
|
|
|
const action = subTrig.getAttribute('data-task-submenu-trigger');
|
|
const action = subTrig.getAttribute('data-task-submenu-trigger');
|
|
|
- const list = taskMenu.querySelector('[data-task-submenu-list="' + action + '"]');
|
|
|
|
|
- const wasHidden = list && list.classList.contains('hidden');
|
|
|
|
|
- closeAllTaskSubmenus();
|
|
|
|
|
- if (list && wasHidden) { list.classList.remove('hidden'); }
|
|
|
|
|
|
|
+ if (taskMenuAction === action && !taskMenuFlyout.classList.contains('hidden')) {
|
|
|
|
|
+ closeTaskMenuFlyout();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ openTaskMenuFlyout(action);
|
|
|
|
|
+ }
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1179,7 +1254,7 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const it = ev.target.closest('[data-task-menu-item]');
|
|
const it = ev.target.closest('[data-task-menu-item]');
|
|
|
- if (it && taskMenu.contains(it)) {
|
|
|
|
|
|
|
+ if (it && taskMenu.contains(it) && !it.hasAttribute('data-task-submenu-trigger')) {
|
|
|
ev.preventDefault();
|
|
ev.preventDefault();
|
|
|
const action = it.getAttribute('data-task-menu-item');
|
|
const action = it.getAttribute('data-task-menu-item');
|
|
|
const taskId = taskMenuTaskId;
|
|
const taskId = taskMenuTaskId;
|
|
@@ -1272,20 +1347,11 @@
|
|
|
if (tr) {
|
|
if (tr) {
|
|
|
tr.setAttribute('data-description', desc);
|
|
tr.setAttribute('data-description', desc);
|
|
|
tr.setAttribute('data-url', url);
|
|
tr.setAttribute('data-url', url);
|
|
|
- const link = qs('[data-task-url-link]', tr);
|
|
|
|
|
- const infoBtn = qs('[data-task-info-trigger]', tr);
|
|
|
|
|
|
|
+ const link = qs('[data-task-url-link]', tr);
|
|
|
if (link) {
|
|
if (link) {
|
|
|
link.href = url;
|
|
link.href = url;
|
|
|
link.classList.toggle('hidden', url === '');
|
|
link.classList.toggle('hidden', url === '');
|
|
|
}
|
|
}
|
|
|
- if (infoBtn) {
|
|
|
|
|
- const refsRaw = tr.getAttribute('data-links') || '[]';
|
|
|
|
|
- let hasLinks = false;
|
|
|
|
|
- try { hasLinks = Array.isArray(JSON.parse(refsRaw)) && JSON.parse(refsRaw).length > 0; }
|
|
|
|
|
- catch (_) { hasLinks = false; }
|
|
|
|
|
- const showInfo = desc !== '' || url !== '' || hasLinks;
|
|
|
|
|
- infoBtn.classList.toggle('hidden', !showInfo);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
closeDetailsModal();
|
|
closeDetailsModal();
|
|
|
flash('Saved');
|
|
flash('Saved');
|
|
@@ -1299,180 +1365,6 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // --- Task info popover (click on the per-row info icon) -------------
|
|
|
|
|
- //
|
|
|
|
|
- // Single body-attached panel that opens to the right of the trigger,
|
|
|
|
|
- // vertically centred on it. Read-only — shows the task title,
|
|
|
|
|
- // description, URL link, and linked-task references. Closing
|
|
|
|
|
- // mirrors the cellPopover: outside-pointerdown, Escape, scroll /
|
|
|
|
|
- // resize, and a 250 ms mouseleave grace.
|
|
|
|
|
-
|
|
|
|
|
- let infoPopover = null;
|
|
|
|
|
- let infoPopoverAnchor = null;
|
|
|
|
|
- let infoPopoverGrace = null;
|
|
|
|
|
- let infoPopoverOpenAt = 0;
|
|
|
|
|
-
|
|
|
|
|
- function cancelInfoPopoverGrace() {
|
|
|
|
|
- if (infoPopoverGrace) { clearTimeout(infoPopoverGrace); infoPopoverGrace = null; }
|
|
|
|
|
- }
|
|
|
|
|
- function scheduleInfoPopoverGrace() {
|
|
|
|
|
- cancelInfoPopoverGrace();
|
|
|
|
|
- infoPopoverGrace = setTimeout(closeInfoPopover, 250);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function buildInfoPopover() {
|
|
|
|
|
- if (infoPopover) { return infoPopover; }
|
|
|
|
|
- const r = document.createElement('div');
|
|
|
|
|
- r.className = 'task-info-popover hidden';
|
|
|
|
|
- r.setAttribute('role', 'dialog');
|
|
|
|
|
- r.setAttribute('aria-label', 'Task info');
|
|
|
|
|
- document.body.appendChild(r);
|
|
|
|
|
- r.addEventListener('mouseenter', cancelInfoPopoverGrace);
|
|
|
|
|
- r.addEventListener('mouseleave', scheduleInfoPopoverGrace);
|
|
|
|
|
- infoPopover = r;
|
|
|
|
|
- return r;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function renderInfoPopoverContent(tr) {
|
|
|
|
|
- if (!infoPopover) { return; }
|
|
|
|
|
- const title = tr.getAttribute('data-task-title') || '';
|
|
|
|
|
- const desc = tr.getAttribute('data-description') || '';
|
|
|
|
|
- const url = tr.getAttribute('data-url') || '';
|
|
|
|
|
- let links = [];
|
|
|
|
|
- try {
|
|
|
|
|
- const parsed = JSON.parse(tr.getAttribute('data-links') || '[]');
|
|
|
|
|
- if (Array.isArray(parsed)) { links = parsed; }
|
|
|
|
|
- } catch (_) { /* ignore */ }
|
|
|
|
|
-
|
|
|
|
|
- infoPopover.innerHTML = '';
|
|
|
|
|
-
|
|
|
|
|
- const head = document.createElement('div');
|
|
|
|
|
- head.className = 'task-info-title';
|
|
|
|
|
- head.textContent = title;
|
|
|
|
|
- infoPopover.appendChild(head);
|
|
|
|
|
-
|
|
|
|
|
- if (desc) {
|
|
|
|
|
- const body = document.createElement('div');
|
|
|
|
|
- body.className = 'task-info-desc';
|
|
|
|
|
- body.textContent = desc;
|
|
|
|
|
- infoPopover.appendChild(body);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (url) {
|
|
|
|
|
- const p = document.createElement('div');
|
|
|
|
|
- p.className = 'task-info-url';
|
|
|
|
|
- const a = document.createElement('a');
|
|
|
|
|
- a.href = url;
|
|
|
|
|
- a.target = '_blank';
|
|
|
|
|
- a.rel = 'noopener noreferrer';
|
|
|
|
|
- a.textContent = url;
|
|
|
|
|
- p.appendChild(a);
|
|
|
|
|
- infoPopover.appendChild(p);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (links.length > 0) {
|
|
|
|
|
- const refsHead = document.createElement('div');
|
|
|
|
|
- refsHead.className = 'task-info-refs-head';
|
|
|
|
|
- refsHead.textContent = 'Referenced tasks';
|
|
|
|
|
- infoPopover.appendChild(refsHead);
|
|
|
|
|
-
|
|
|
|
|
- const list = document.createElement('ul');
|
|
|
|
|
- list.className = 'task-info-refs';
|
|
|
|
|
- links.forEach(function (l) {
|
|
|
|
|
- if (!l || typeof l !== 'object') { return; }
|
|
|
|
|
- const li = document.createElement('li');
|
|
|
|
|
- const dir = l.direction === 'source' ? 'Copied from' : 'Copied to';
|
|
|
|
|
- const arr = l.direction === 'source' ? '←' : '→';
|
|
|
|
|
- const a = document.createElement('a');
|
|
|
|
|
- a.href = '/sprints/' + l.sprint_id;
|
|
|
|
|
- a.textContent = arr + ' ' + (l.title || '') + ' (' + (l.sprint_name || '') + ')';
|
|
|
|
|
- a.title = dir + ': ' + (l.title || '') + ' (' + (l.sprint_name || '') + ')';
|
|
|
|
|
- li.appendChild(a);
|
|
|
|
|
- list.appendChild(li);
|
|
|
|
|
- });
|
|
|
|
|
- infoPopover.appendChild(list);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function positionInfoPopover() {
|
|
|
|
|
- if (!infoPopover || !infoPopoverAnchor) { return; }
|
|
|
|
|
- const rect = infoPopoverAnchor.getBoundingClientRect();
|
|
|
|
|
- const ph = infoPopover.offsetHeight;
|
|
|
|
|
- const pw = infoPopover.offsetWidth;
|
|
|
|
|
- const vw = document.documentElement.clientWidth;
|
|
|
|
|
- const vh = document.documentElement.clientHeight;
|
|
|
|
|
- let top = window.scrollY + rect.top + rect.height / 2 - ph / 2;
|
|
|
|
|
- let left = window.scrollX + rect.right + 8;
|
|
|
|
|
- if (left + pw > window.scrollX + vw - 8) {
|
|
|
|
|
- left = window.scrollX + rect.left - pw - 8;
|
|
|
|
|
- }
|
|
|
|
|
- if (top < window.scrollY + 8) { top = window.scrollY + 8; }
|
|
|
|
|
- if (top + ph > window.scrollY + vh - 8) {
|
|
|
|
|
- top = window.scrollY + vh - ph - 8;
|
|
|
|
|
- }
|
|
|
|
|
- infoPopover.style.top = top + 'px';
|
|
|
|
|
- infoPopover.style.left = left + 'px';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function openInfoPopover(trigger) {
|
|
|
|
|
- const tr = trigger.closest('tr[data-task-row]');
|
|
|
|
|
- if (!tr) { return; }
|
|
|
|
|
- buildInfoPopover();
|
|
|
|
|
- infoPopoverAnchor = trigger;
|
|
|
|
|
- renderInfoPopoverContent(tr);
|
|
|
|
|
- cancelInfoPopoverGrace();
|
|
|
|
|
- infoPopover.classList.remove('hidden');
|
|
|
|
|
- trigger.setAttribute('aria-expanded', 'true');
|
|
|
|
|
- positionInfoPopover();
|
|
|
|
|
- infoPopoverOpenAt = Date.now();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function closeInfoPopover() {
|
|
|
|
|
- cancelInfoPopoverGrace();
|
|
|
|
|
- if (infoPopover) { infoPopover.classList.add('hidden'); }
|
|
|
|
|
- if (infoPopoverAnchor) { infoPopoverAnchor.setAttribute('aria-expanded', 'false'); }
|
|
|
|
|
- infoPopoverAnchor = null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- on(root, 'click', '[data-task-info-trigger]', function (ev) {
|
|
|
|
|
- ev.preventDefault();
|
|
|
|
|
- ev.stopPropagation();
|
|
|
|
|
- if (infoPopoverAnchor === this && infoPopover && !infoPopover.classList.contains('hidden')) {
|
|
|
|
|
- closeInfoPopover();
|
|
|
|
|
- } else {
|
|
|
|
|
- openInfoPopover(this);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('pointerdown', function (ev) {
|
|
|
|
|
- if (!infoPopover || infoPopover.classList.contains('hidden')) { return; }
|
|
|
|
|
- if (Date.now() - infoPopoverOpenAt < 50) { return; }
|
|
|
|
|
- if (infoPopover.contains(ev.target)) { return; }
|
|
|
|
|
- if (infoPopoverAnchor && infoPopoverAnchor.contains(ev.target)) { return; }
|
|
|
|
|
- closeInfoPopover();
|
|
|
|
|
- }, true);
|
|
|
|
|
-
|
|
|
|
|
- document.addEventListener('keydown', function (ev) {
|
|
|
|
|
- if (ev.key === 'Escape' && infoPopover && !infoPopover.classList.contains('hidden')) {
|
|
|
|
|
- closeInfoPopover();
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Focus loss on the trigger or links inside the popover closes after a
|
|
|
|
|
- // short grace (matches the "focus lost" close hint in the spec).
|
|
|
|
|
- document.addEventListener('focusout', function (ev) {
|
|
|
|
|
- if (!infoPopover || infoPopover.classList.contains('hidden')) { return; }
|
|
|
|
|
- const next = ev.relatedTarget;
|
|
|
|
|
- if (next && (infoPopover.contains(next) || (infoPopoverAnchor && infoPopoverAnchor.contains(next)))) { return; }
|
|
|
|
|
- scheduleInfoPopoverGrace();
|
|
|
|
|
- }, true);
|
|
|
|
|
-
|
|
|
|
|
- window.addEventListener('scroll', function () {
|
|
|
|
|
- if (infoPopover && !infoPopover.classList.contains('hidden')) { closeInfoPopover(); }
|
|
|
|
|
- }, true);
|
|
|
|
|
- window.addEventListener('resize', function () {
|
|
|
|
|
- if (infoPopover && !infoPopover.classList.contains('hidden')) { closeInfoPopover(); }
|
|
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
// --- Click-pickup reorder -------------------------------------------
|
|
// --- Click-pickup reorder -------------------------------------------
|
|
|
//
|
|
//
|