|
@@ -356,6 +356,41 @@
|
|
|
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) {
|
|
|
assignments = assignments || {};
|
|
assignments = assignments || {};
|
|
@@ -368,6 +403,8 @@
|
|
|
tr.setAttribute('data-sort-order', String(task.sort_order));
|
|
tr.setAttribute('data-sort-order', String(task.sort_order));
|
|
|
tr.setAttribute('data-description', task.description || '');
|
|
tr.setAttribute('data-description', task.description || '');
|
|
|
tr.setAttribute('data-url', task.url || '');
|
|
tr.setAttribute('data-url', task.url || '');
|
|
|
|
|
+ tr.setAttribute('data-task-title', task.title || '');
|
|
|
|
|
+ tr.setAttribute('data-links', JSON.stringify(Array.isArray(task.links) ? task.links : []));
|
|
|
|
|
|
|
|
// hamburger trigger
|
|
// hamburger trigger
|
|
|
const tdMenu = document.createElement('td');
|
|
const tdMenu = document.createElement('td');
|
|
@@ -387,22 +424,25 @@
|
|
|
tdMenu.appendChild(trig);
|
|
tdMenu.appendChild(trig);
|
|
|
tr.appendChild(tdMenu);
|
|
tr.appendChild(tdMenu);
|
|
|
|
|
|
|
|
- // title cell with URL + description affordances
|
|
|
|
|
|
|
+ // title cell — grid layout: [title input] [URL + ref icons] [info icon]
|
|
|
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 = 'flex items-center gap-1.5';
|
|
|
|
|
|
|
+ titleWrap.className = 'task-title-grid';
|
|
|
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 = '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';
|
|
|
|
|
|
|
+ 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';
|
|
|
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';
|
|
|
- urlLink.rel = 'noopener';
|
|
|
|
|
|
|
+ urlLink.rel = 'noopener noreferrer';
|
|
|
urlLink.href = task.url || '';
|
|
urlLink.href = task.url || '';
|
|
|
urlLink.title = 'Open task link';
|
|
urlLink.title = 'Open task link';
|
|
|
urlLink.setAttribute('aria-label', 'Open task link');
|
|
urlLink.setAttribute('aria-label', 'Open task link');
|
|
@@ -413,21 +453,32 @@
|
|
|
+ '<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>';
|
|
|
- titleWrap.appendChild(urlLink);
|
|
|
|
|
-
|
|
|
|
|
- const descBtn = document.createElement('button');
|
|
|
|
|
- descBtn.type = 'button';
|
|
|
|
|
- descBtn.setAttribute('data-task-desc-trigger', '');
|
|
|
|
|
- descBtn.className = 'task-desc-trigger 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';
|
|
|
|
|
- descBtn.title = 'View description';
|
|
|
|
|
- descBtn.setAttribute('aria-label', 'View description');
|
|
|
|
|
- if (!task.description) { descBtn.classList.add('hidden'); }
|
|
|
|
|
- descBtn.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">'
|
|
|
|
|
- + '<rect x="2" y="3" width="12" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/>'
|
|
|
|
|
- + '<line x1="4" y1="6" x2="12" y2="6" stroke="currentColor" stroke-width="1.5"/>'
|
|
|
|
|
- + '<line x1="4" y1="9" x2="10" y2="9" stroke="currentColor" stroke-width="1.5"/>'
|
|
|
|
|
|
|
+ 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>';
|
|
+ '</svg>';
|
|
|
- titleWrap.appendChild(descBtn);
|
|
|
|
|
|
|
+ titleWrap.appendChild(infoBtn);
|
|
|
|
|
|
|
|
tdTitle.appendChild(titleWrap);
|
|
tdTitle.appendChild(titleWrap);
|
|
|
tr.appendChild(tdTitle);
|
|
tr.appendChild(tdTitle);
|
|
@@ -1222,13 +1273,18 @@
|
|
|
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 link = qs('[data-task-url-link]', tr);
|
|
|
- const descBtn = qs('[data-task-desc-trigger]', tr);
|
|
|
|
|
|
|
+ const infoBtn = qs('[data-task-info-trigger]', tr);
|
|
|
if (link) {
|
|
if (link) {
|
|
|
link.href = url;
|
|
link.href = url;
|
|
|
link.classList.toggle('hidden', url === '');
|
|
link.classList.toggle('hidden', url === '');
|
|
|
}
|
|
}
|
|
|
- if (descBtn) {
|
|
|
|
|
- descBtn.classList.toggle('hidden', desc === '');
|
|
|
|
|
|
|
+ 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();
|
|
@@ -1243,76 +1299,181 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // --- Description popover (click on the description marker) ----------
|
|
|
|
|
|
|
+ // --- Task info popover (click on the per-row info icon) -------------
|
|
|
//
|
|
//
|
|
|
- // Read-only popover so non-admins can see the description, and admins
|
|
|
|
|
- // can peek without opening the full edit modal.
|
|
|
|
|
|
|
+ // 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 descPopover = null;
|
|
|
|
|
- let descPopoverAnchor = null;
|
|
|
|
|
- let descPopoverOpenAt = 0;
|
|
|
|
|
|
|
+ let infoPopover = null;
|
|
|
|
|
+ let infoPopoverAnchor = null;
|
|
|
|
|
+ let infoPopoverGrace = null;
|
|
|
|
|
+ let infoPopoverOpenAt = 0;
|
|
|
|
|
|
|
|
- function buildDescPopover() {
|
|
|
|
|
- if (descPopover) { return descPopover; }
|
|
|
|
|
|
|
+ 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');
|
|
const r = document.createElement('div');
|
|
|
- r.className = 'task-desc-popover hidden';
|
|
|
|
|
- r.setAttribute('role', 'tooltip');
|
|
|
|
|
|
|
+ r.className = 'task-info-popover hidden';
|
|
|
|
|
+ r.setAttribute('role', 'dialog');
|
|
|
|
|
+ r.setAttribute('aria-label', 'Task info');
|
|
|
document.body.appendChild(r);
|
|
document.body.appendChild(r);
|
|
|
- descPopover = r;
|
|
|
|
|
|
|
+ r.addEventListener('mouseenter', cancelInfoPopoverGrace);
|
|
|
|
|
+ r.addEventListener('mouseleave', scheduleInfoPopoverGrace);
|
|
|
|
|
+ infoPopover = r;
|
|
|
return r;
|
|
return r;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function openDescPopover(anchor, text) {
|
|
|
|
|
- if (!text) { return; }
|
|
|
|
|
- buildDescPopover();
|
|
|
|
|
- descPopoverAnchor = anchor;
|
|
|
|
|
- descPopover.textContent = text;
|
|
|
|
|
- descPopover.classList.remove('hidden');
|
|
|
|
|
- const rect = anchor.getBoundingClientRect();
|
|
|
|
|
- const ph = descPopover.offsetHeight;
|
|
|
|
|
- const pw = descPopover.offsetWidth;
|
|
|
|
|
|
|
+ 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 vw = document.documentElement.clientWidth;
|
|
|
- let top = window.scrollY + rect.bottom + 4;
|
|
|
|
|
- let left = window.scrollX + rect.left;
|
|
|
|
|
|
|
+ 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) {
|
|
if (left + pw > window.scrollX + vw - 8) {
|
|
|
- left = window.scrollX + vw - pw - 8;
|
|
|
|
|
|
|
+ left = window.scrollX + rect.left - pw - 8;
|
|
|
}
|
|
}
|
|
|
- if (top + ph > window.scrollY + document.documentElement.clientHeight - 8) {
|
|
|
|
|
- top = window.scrollY + rect.top - ph - 4;
|
|
|
|
|
|
|
+ if (top < window.scrollY + 8) { top = window.scrollY + 8; }
|
|
|
|
|
+ if (top + ph > window.scrollY + vh - 8) {
|
|
|
|
|
+ top = window.scrollY + vh - ph - 8;
|
|
|
}
|
|
}
|
|
|
- descPopover.style.top = top + 'px';
|
|
|
|
|
- descPopover.style.left = left + 'px';
|
|
|
|
|
- descPopoverOpenAt = Date.now();
|
|
|
|
|
|
|
+ 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 closeDescPopover() {
|
|
|
|
|
- if (descPopover) { descPopover.classList.add('hidden'); }
|
|
|
|
|
- descPopoverAnchor = null;
|
|
|
|
|
|
|
+ function closeInfoPopover() {
|
|
|
|
|
+ cancelInfoPopoverGrace();
|
|
|
|
|
+ if (infoPopover) { infoPopover.classList.add('hidden'); }
|
|
|
|
|
+ if (infoPopoverAnchor) { infoPopoverAnchor.setAttribute('aria-expanded', 'false'); }
|
|
|
|
|
+ infoPopoverAnchor = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- on(root, 'click', '[data-task-desc-trigger]', function (ev) {
|
|
|
|
|
|
|
+ on(root, 'click', '[data-task-info-trigger]', function (ev) {
|
|
|
ev.preventDefault();
|
|
ev.preventDefault();
|
|
|
ev.stopPropagation();
|
|
ev.stopPropagation();
|
|
|
- const tr = this.closest('tr[data-task-row]');
|
|
|
|
|
- if (!tr) { return; }
|
|
|
|
|
- const text = tr.getAttribute('data-description') || '';
|
|
|
|
|
- openDescPopover(this, text);
|
|
|
|
|
|
|
+ if (infoPopoverAnchor === this && infoPopover && !infoPopover.classList.contains('hidden')) {
|
|
|
|
|
+ closeInfoPopover();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ openInfoPopover(this);
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('pointerdown', function (ev) {
|
|
document.addEventListener('pointerdown', function (ev) {
|
|
|
- if (!descPopover || descPopover.classList.contains('hidden')) { return; }
|
|
|
|
|
- if (Date.now() - descPopoverOpenAt < 50) { return; }
|
|
|
|
|
- if (descPopover.contains(ev.target)) { return; }
|
|
|
|
|
- if (descPopoverAnchor && descPopoverAnchor.contains(ev.target)) { return; }
|
|
|
|
|
- closeDescPopover();
|
|
|
|
|
|
|
+ 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);
|
|
}, true);
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function (ev) {
|
|
document.addEventListener('keydown', function (ev) {
|
|
|
- if (ev.key === 'Escape' && descPopover && !descPopover.classList.contains('hidden')) {
|
|
|
|
|
- closeDescPopover();
|
|
|
|
|
|
|
+ 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 -------------------------------------------
|
|
|
//
|
|
//
|
|
|
// The user picks "Move (pick up)" from the menu, the row tracks the
|
|
// The user picks "Move (pick up)" from the menu, the row tracks the
|