feat(itemToolltip): fusion pathTooltip + ownerTooltip into itemToolltip, move it in the header for more visibility
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
/* ── Item tooltip — "technical sheet" ─────────────────────────────────────── */
|
||||||
|
/* */
|
||||||
|
/* Overlays the search bar area in the top-bar while hovering a file item. */
|
||||||
|
/* Spans from the sidebar right-edge to just before .user-controls. */
|
||||||
|
/* */
|
||||||
|
/* 3-column CSS grid: [icon] [label] [value] */
|
||||||
|
/* All values start at the same x position regardless of label width. */
|
||||||
|
|
||||||
|
.path-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
/* Fill most of the top-bar (topbar height 70px, 8px margin top/bottom) */
|
||||||
|
top: 8px;
|
||||||
|
|
||||||
|
/* Desktop: starts at sidebar right edge + top-bar padding */
|
||||||
|
left: calc(var(--sidebar-width) + 30px);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Right edge stops just before .user-controls:
|
||||||
|
* 30px (top-bar right padding) + ~90px (notif bell + gap + avatar btn) + 4px gap
|
||||||
|
* Using left + right instead of width so the browser computes it dynamically.
|
||||||
|
*/
|
||||||
|
right: 124px;
|
||||||
|
|
||||||
|
/* High enough to sit above the top-bar content */
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1em max-content 1fr;
|
||||||
|
column-gap: 0.6em;
|
||||||
|
row-gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
/* Fill the top-bar height minus an 8px margin on each side */
|
||||||
|
height: 54px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--color-border-medium);
|
||||||
|
|
||||||
|
background-color: var(--color-bg-input);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
/* avoid blinking when pointer moves between items */
|
||||||
|
transition: display 0.2s allow-discrete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon column */
|
||||||
|
.path-tooltip__icon {
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-align: center;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label column */
|
||||||
|
.path-tooltip__label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-tooltip__label::after {
|
||||||
|
content: ':';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Value column */
|
||||||
|
.path-tooltip__value {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Path value uses monospace to read folder separators clearly */
|
||||||
|
.path-tooltip__value--path {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "?" placeholder when data is absent */
|
||||||
|
.path-tooltip__value--unknown {
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Narrow screens (sidebar hidden) ────────────────────────────────────── */
|
||||||
|
/* On mobile the sidebar slides off-screen; align after the #sidebar-toggle */
|
||||||
|
/* button (≈44px) that takes its place in the top-bar. */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.path-tooltip {
|
||||||
|
/* 16px topbar padding + ~44px sidebar-toggle + 8px gap */
|
||||||
|
left: 68px;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Right edge stops before search-toggle-btn + user-controls:
|
||||||
|
* ~40px (search-toggle) + 12px (gap) + ~90px (user-controls) + 16px (padding)
|
||||||
|
*/
|
||||||
|
right: 158px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Vignette inside tooltip ─────────────────────────────────────────────── */
|
||||||
|
/* The vignette name hard-codes 14px; scale it down to match the tooltip. */
|
||||||
|
|
||||||
|
.path-tooltip .user-vignette {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-tooltip .user-vignette__name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
.path-tooltip {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 8px;
|
|
||||||
left: calc(var(--sidebar-width) + 8px);
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
max-width: 120ch;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
background-color: var(--color-bg-subtle);
|
|
||||||
border: 1px solid var(--color-border-faint);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-family: monospace;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
/* avoid blinking when pointer change items */
|
|
||||||
transition: display 0.2s allow-discrete;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
@import url("./components/search.css");
|
@import url("./components/search.css");
|
||||||
@import url("./components/icons.css");
|
@import url("./components/icons.css");
|
||||||
@import url("./components/csp-utilities.css");
|
@import url("./components/csp-utilities.css");
|
||||||
@import url("./components/pathTooltip.css");
|
@import url("./components/itemTooltip.css");
|
||||||
|
|
||||||
/* Theme */
|
/* Theme */
|
||||||
@import url("./themes/dark.css");
|
@import url("./themes/dark.css");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { escapeHtml, formatDateTime } from '../core/formatters.js';
|
|||||||
import { i18n } from '../core/i18n.js';
|
import { i18n } from '../core/i18n.js';
|
||||||
import { batchToolbar } from '../features/files/batchToolbar.js';
|
import { batchToolbar } from '../features/files/batchToolbar.js';
|
||||||
import { fileOps } from '../features/files/fileOperations.js';
|
import { fileOps } from '../features/files/fileOperations.js';
|
||||||
import * as pathTooltip from '../features/pathTooltip.js';
|
import * as itemTooltip from '../features/itemTooltip.js';
|
||||||
import { appElements } from './state.js';
|
import { appElements } from './state.js';
|
||||||
import { ui } from './ui.js';
|
import { ui } from './ui.js';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ async function loadTrashItems() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (batchToolbar) batchToolbar.clear();
|
if (batchToolbar) batchToolbar.clear();
|
||||||
pathTooltip.destroy(elements.filesList);
|
itemTooltip.destroy(elements.filesList);
|
||||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
elements.filesList.innerHTML = `
|
elements.filesList.innerHTML = `
|
||||||
<div class="list-header trash-header">
|
<div class="list-header trash-header">
|
||||||
@@ -50,7 +50,7 @@ async function loadTrashItems() {
|
|||||||
trashItems.forEach((item) => {
|
trashItems.forEach((item) => {
|
||||||
addTrashItemToView(item);
|
addTrashItemToView(item);
|
||||||
});
|
});
|
||||||
pathTooltip.init(elements.filesList);
|
itemTooltip.init(elements.filesList);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading trash items:', error);
|
console.error('Error loading trash items:', error);
|
||||||
ui.showNotification('Error', 'Error loading trash items');
|
ui.showNotification('Error', 'Error loading trash items');
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ export class ResourceListComponent {
|
|||||||
el.dataset.folderName = folder.name;
|
el.dataset.folderName = folder.name;
|
||||||
el.dataset.parentId = folder.parent_id || '';
|
el.dataset.parentId = folder.parent_id || '';
|
||||||
if (folder.path) el.dataset.path = folder.path;
|
if (folder.path) el.dataset.path = folder.path;
|
||||||
|
if (folder.owner_id) el.dataset.ownerId = folder.owner_id;
|
||||||
if (cfg.draggable) el.setAttribute('draggable', 'true');
|
if (cfg.draggable) el.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
const isFav = cfg.isFavorite ? cfg.isFavorite(folder.id, 'folder') : false;
|
const isFav = cfg.isFavorite ? cfg.isFavorite(folder.id, 'folder') : false;
|
||||||
@@ -463,6 +464,7 @@ export class ResourceListComponent {
|
|||||||
el.dataset.fileName = file.name;
|
el.dataset.fileName = file.name;
|
||||||
el.dataset.folderId = file.folder_id || '';
|
el.dataset.folderId = file.folder_id || '';
|
||||||
if (file.path) el.dataset.path = file.path;
|
if (file.path) el.dataset.path = file.path;
|
||||||
|
if (file.owner_id) el.dataset.ownerId = file.owner_id;
|
||||||
if (cfg.draggable) el.setAttribute('draggable', 'true');
|
if (cfg.draggable) el.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|||||||
@@ -263,6 +263,10 @@ const OxiIcons = {
|
|||||||
512,
|
512,
|
||||||
'M40 48C26.7 48 16 58.7 16 72l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24L40 48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM16 232l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0z'
|
'M40 48C26.7 48 16 58.7 16 72l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24L40 48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM16 232l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0z'
|
||||||
],
|
],
|
||||||
|
'location-crosshairs': [
|
||||||
|
576,
|
||||||
|
'M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM128 256a160 160 0 1 0 320 0 160 160 0 1 0 -320 0zm160-96a96 96 0 1 1 0 192 96 96 0 1 1 0-192z'
|
||||||
|
],
|
||||||
lock: [
|
lock: [
|
||||||
448,
|
448,
|
||||||
'M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z'
|
'M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z'
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item tooltip — unified hover tooltip showing a stable "technical sheet" for
|
||||||
|
* a hovered `.file-item`.
|
||||||
|
*
|
||||||
|
* Both rows are always rendered so the layout never shifts between items.
|
||||||
|
* A "?" placeholder is shown when data is absent for a given row.
|
||||||
|
*
|
||||||
|
* data-owner-id → 👤 Owner [userVignette] (avatar + name, async)
|
||||||
|
* data-path → ⊕ Path Documents/Work (monospace)
|
||||||
|
*
|
||||||
|
* The tooltip is shown only when at least one of the two attributes is present.
|
||||||
|
* Lines are laid out in a 3-column CSS grid (icon | label | value) so values
|
||||||
|
* are always left-aligned at the same x position.
|
||||||
|
*
|
||||||
|
* Replaces the former `pathTooltip` and `ownerTooltip` modules.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import * as itemTooltip from '../features/itemTooltip.js';
|
||||||
|
* itemTooltip.init(containerEl) — call after rendering items
|
||||||
|
* itemTooltip.destroy(containerEl) — call when leaving the section
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createUserVignette } from '../components/userVignette.js';
|
||||||
|
import { i18n } from '../core/i18n.js';
|
||||||
|
import { systemUsers } from '../model/systemUsers.js';
|
||||||
|
|
||||||
|
// ── Tooltip DOM ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** @returns {HTMLElement} */
|
||||||
|
function _getOrCreateTooltip() {
|
||||||
|
let el = document.getElementById('path-tooltip');
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.id = 'path-tooltip';
|
||||||
|
el.className = 'path-tooltip hidden';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hide() {
|
||||||
|
const el = document.getElementById('path-tooltip');
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Row builder ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append one grid row (icon | label | value) to the tooltip container.
|
||||||
|
* The three cells are direct children of the CSS grid — column assignment
|
||||||
|
* is automatic.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} tooltip
|
||||||
|
* @param {string} iconClass FontAwesome class string, e.g. `"fas fa-user"`
|
||||||
|
* @param {string} labelText
|
||||||
|
* @param {(el: HTMLElement) => void} populate Fills the value cell.
|
||||||
|
* @returns {HTMLElement} The value cell.
|
||||||
|
*/
|
||||||
|
function _addRow(tooltip, iconClass, labelText, populate) {
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.className = `${iconClass} path-tooltip__icon`;
|
||||||
|
tooltip.appendChild(icon);
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'path-tooltip__label';
|
||||||
|
label.textContent = labelText;
|
||||||
|
tooltip.appendChild(label);
|
||||||
|
|
||||||
|
const value = document.createElement('span');
|
||||||
|
value.className = 'path-tooltip__value';
|
||||||
|
populate(value);
|
||||||
|
tooltip.appendChild(value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a "?" placeholder cell (used when data is unavailable).
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
function _setUnknown(el) {
|
||||||
|
el.classList.add('path-tooltip__value--unknown');
|
||||||
|
el.textContent = '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Event handler ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MouseEvent} e
|
||||||
|
*/
|
||||||
|
function _onEnter(e) {
|
||||||
|
const item = /** @type {HTMLElement} */ (e.currentTarget);
|
||||||
|
const ownerId = item.dataset.ownerId;
|
||||||
|
const path = item.dataset.path;
|
||||||
|
|
||||||
|
// Nothing to show — don't display an all-? tooltip.
|
||||||
|
if (!ownerId && !path) return;
|
||||||
|
|
||||||
|
const tooltip = _getOrCreateTooltip();
|
||||||
|
|
||||||
|
// Clear previous content.
|
||||||
|
while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild);
|
||||||
|
|
||||||
|
// ── Owner row (always rendered) ───────────────────────────────────────────
|
||||||
|
_addRow(tooltip, 'fas fa-user', i18n.t('files.owner', 'Owner'), (el) => {
|
||||||
|
if (ownerId && systemUsers.isAvailable()) {
|
||||||
|
el.appendChild(createUserVignette(ownerId, 'xs'));
|
||||||
|
} else {
|
||||||
|
_setUnknown(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Path row (always rendered) ────────────────────────────────────────────
|
||||||
|
_addRow(tooltip, 'fas fa-location-crosshairs', i18n.t('tooltip.path', 'Path'), (el) => {
|
||||||
|
if (path) {
|
||||||
|
el.classList.add('path-tooltip__value--path');
|
||||||
|
el.textContent = path;
|
||||||
|
} else {
|
||||||
|
_setUnknown(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltip.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onLeave() {
|
||||||
|
_hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listener registry (WeakMap for leak-free cleanup) ────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ enter: (e: MouseEvent) => void, leave: () => void }} Handlers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {WeakMap<HTMLElement, Handlers>} */
|
||||||
|
const _registry = new WeakMap();
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach tooltip listeners to every `.file-item` inside `container`.
|
||||||
|
* Items with neither `data-owner-id` nor `data-path` will not trigger the
|
||||||
|
* tooltip. Safe to call repeatedly — already-wired elements are skipped.
|
||||||
|
* @param {HTMLElement} container
|
||||||
|
*/
|
||||||
|
function init(container) {
|
||||||
|
for (const item of container.querySelectorAll('.file-item')) {
|
||||||
|
const el = /** @type {HTMLElement} */ (item);
|
||||||
|
if (_registry.has(el)) continue; // already wired
|
||||||
|
|
||||||
|
const enter = (/** @type {MouseEvent} */ ev) => _onEnter(ev);
|
||||||
|
const leave = () => _onLeave();
|
||||||
|
|
||||||
|
el.addEventListener('mouseenter', enter);
|
||||||
|
el.addEventListener('mouseleave', leave);
|
||||||
|
_registry.set(el, { enter, leave });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove tooltip listeners from all `.file-item` elements inside `container`
|
||||||
|
* and hide any visible tooltip.
|
||||||
|
* @param {HTMLElement} container
|
||||||
|
*/
|
||||||
|
function destroy(container) {
|
||||||
|
for (const item of container.querySelectorAll('.file-item')) {
|
||||||
|
const el = /** @type {HTMLElement} */ (item);
|
||||||
|
const h = _registry.get(el);
|
||||||
|
if (h) {
|
||||||
|
el.removeEventListener('mouseenter', h.enter);
|
||||||
|
el.removeEventListener('mouseleave', h.leave);
|
||||||
|
_registry.delete(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { destroy, init };
|
||||||
@@ -11,7 +11,7 @@ import { ResourceListComponent } from '../../components/resourceList.js';
|
|||||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { batchToolbar } from '../files/batchToolbar.js';
|
import { batchToolbar } from '../files/batchToolbar.js';
|
||||||
import * as pathTooltip from '../pathTooltip.js';
|
import * as itemTooltip from '../itemTooltip.js';
|
||||||
|
|
||||||
/** @import {FileItem, FolderItem, ItemTypeEnum, RecentItem} from '../../core/types.js' */
|
/** @import {FileItem, FolderItem, ItemTypeEnum, RecentItem} from '../../core/types.js' */
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ const recent = {
|
|||||||
}
|
}
|
||||||
batchToolbar.setActiveComponent(this._component);
|
batchToolbar.setActiveComponent(this._component);
|
||||||
this._component.render(items);
|
this._component.render(items);
|
||||||
pathTooltip.init(filesList);
|
itemTooltip.init(filesList);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error displaying recent files:', error);
|
console.error('Error displaying recent files:', error);
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Owner tooltip — shows "Shared by: <display name>" when hovering a
|
|
||||||
* `.file-item[data-owner-id]` element.
|
|
||||||
*
|
|
||||||
* Reuses the existing `#path-tooltip` DOM element (same position and style)
|
|
||||||
* so no extra CSS is needed. The tooltip is hidden immediately on mouseleave
|
|
||||||
* and the display-name resolution is async-but-usually-instant because
|
|
||||||
* `systemUsers` is pre-fetched when the Shared-with-me section is entered.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ownerTooltip.init(containerEl) — call after rendering items
|
|
||||||
* ownerTooltip.destroy(containerEl) — call when leaving the section
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '../core/i18n.js';
|
|
||||||
import { systemUsers } from '../model/systemUsers.js';
|
|
||||||
|
|
||||||
// ── Tooltip DOM ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** @returns {HTMLElement} */
|
|
||||||
function _getOrCreateTooltip() {
|
|
||||||
let el = document.getElementById('path-tooltip');
|
|
||||||
if (!el) {
|
|
||||||
el = document.createElement('div');
|
|
||||||
el.id = 'path-tooltip';
|
|
||||||
el.className = 'path-tooltip hidden';
|
|
||||||
document.querySelector('.main-content')?.appendChild(el);
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _hide() {
|
|
||||||
document.getElementById('path-tooltip')?.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Event handlers ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MouseEvent} e
|
|
||||||
*/
|
|
||||||
async function _onEnter(e) {
|
|
||||||
const item = /** @type {HTMLElement} */ (e.currentTarget);
|
|
||||||
const ownerId = item.dataset.ownerId;
|
|
||||||
if (!ownerId) return;
|
|
||||||
|
|
||||||
if (!systemUsers.isAvailable()) return;
|
|
||||||
|
|
||||||
const tooltip = _getOrCreateTooltip();
|
|
||||||
|
|
||||||
// Show immediately with a placeholder so the tooltip appears without lag.
|
|
||||||
const label = i18n.t('sharedwithme_sharedBy', 'Shared by');
|
|
||||||
tooltip.textContent = `${label}: …`;
|
|
||||||
tooltip.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Resolve the name (usually instant from the pre-fetched cache).
|
|
||||||
const name = await systemUsers.getDisplayName(ownerId);
|
|
||||||
|
|
||||||
// Guard: don't update if the user already moved away.
|
|
||||||
if (!tooltip.classList.contains('hidden')) {
|
|
||||||
tooltip.textContent = `${label}: ${name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onLeave() {
|
|
||||||
_hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Listener registry (WeakMap for leak-free cleanup) ────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{ enter: (e: MouseEvent) => void, leave: () => void }} Handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {WeakMap<HTMLElement, Handlers>} */
|
|
||||||
const _registry = new WeakMap();
|
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach owner-tooltip listeners to every `.file-item[data-owner-id]`
|
|
||||||
* inside `container`.
|
|
||||||
* @param {HTMLElement} container
|
|
||||||
*/
|
|
||||||
function init(container) {
|
|
||||||
for (const item of container.querySelectorAll('.file-item[data-owner-id]')) {
|
|
||||||
const el = /** @type {HTMLElement} */ (item);
|
|
||||||
if (_registry.has(el)) continue; // already wired
|
|
||||||
|
|
||||||
/** @type {(e: MouseEvent) => void} */
|
|
||||||
const enter = (e) => {
|
|
||||||
_onEnter(e);
|
|
||||||
}; // intentionally discard the Promise
|
|
||||||
const leave = () => _onLeave();
|
|
||||||
|
|
||||||
el.addEventListener('mouseenter', enter);
|
|
||||||
el.addEventListener('mouseleave', leave);
|
|
||||||
_registry.set(el, { enter, leave });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove owner-tooltip listeners from all `.file-item` elements inside
|
|
||||||
* `container` and hide any visible tooltip.
|
|
||||||
* @param {HTMLElement} container
|
|
||||||
*/
|
|
||||||
function destroy(container) {
|
|
||||||
for (const item of container.querySelectorAll('.file-item')) {
|
|
||||||
const el = /** @type {HTMLElement} */ (item);
|
|
||||||
const h = _registry.get(el);
|
|
||||||
if (h) {
|
|
||||||
el.removeEventListener('mouseenter', h.enter);
|
|
||||||
el.removeEventListener('mouseleave', h.leave);
|
|
||||||
_registry.delete(el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ownerTooltip = { init, destroy };
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* Path tooltip — shows the full path of a hovered file/folder item
|
|
||||||
* in an overlay at the bottom-left of the content area.
|
|
||||||
*
|
|
||||||
* Usage: call init(container) after rendering items, destroy(container) on teardown.
|
|
||||||
* Only file-item elements with a data-path attribute trigger the tooltip.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {HTMLElement|null} */
|
|
||||||
let _tooltip = null;
|
|
||||||
|
|
||||||
function _getOrCreateTooltip() {
|
|
||||||
if (_tooltip) return _tooltip;
|
|
||||||
_tooltip = document.getElementById('path-tooltip');
|
|
||||||
if (!_tooltip) {
|
|
||||||
_tooltip = document.createElement('div');
|
|
||||||
_tooltip.id = 'path-tooltip';
|
|
||||||
_tooltip.className = 'path-tooltip hidden';
|
|
||||||
document.querySelector('.main-content')?.appendChild(_tooltip);
|
|
||||||
}
|
|
||||||
return _tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MouseEvent} e
|
|
||||||
*/
|
|
||||||
function _onEnter(e) {
|
|
||||||
const item = /** @type {HTMLElement} */ (e.currentTarget);
|
|
||||||
const path = item.dataset.path;
|
|
||||||
if (!path) return;
|
|
||||||
|
|
||||||
const tooltip = _getOrCreateTooltip();
|
|
||||||
tooltip.textContent = path;
|
|
||||||
tooltip.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function _onLeave() {
|
|
||||||
_tooltip?.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} EnterLeaveF
|
|
||||||
* @property {(e: MouseEvent) => void} enter
|
|
||||||
* @property {(e: MouseEvent) => void} leave
|
|
||||||
*
|
|
||||||
|
|
||||||
/** @type {WeakMap<HTMLElement, EnterLeaveF>} */
|
|
||||||
const _listeners = new WeakMap();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach path tooltip listeners to all file-item elements inside container.
|
|
||||||
* @param {HTMLElement} container
|
|
||||||
*/
|
|
||||||
function init(container) {
|
|
||||||
const items = container.querySelectorAll('.file-item[data-path]');
|
|
||||||
items.forEach((item) => {
|
|
||||||
const el = /** @type {HTMLElement} */ (item);
|
|
||||||
|
|
||||||
/** @type {(e: MouseEvent) => void} */
|
|
||||||
const enter = (e) => _onEnter(e);
|
|
||||||
el.addEventListener('mouseenter', enter);
|
|
||||||
|
|
||||||
/** @type {(e: MouseEvent) => void} */
|
|
||||||
const leave = (_e) => _onLeave();
|
|
||||||
el.addEventListener('mouseleave', leave);
|
|
||||||
|
|
||||||
_listeners.set(el, { enter, leave });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove path tooltip listeners from all file-item elements inside container.
|
|
||||||
* @param {HTMLElement} container
|
|
||||||
*/
|
|
||||||
function destroy(container) {
|
|
||||||
const items = container.querySelectorAll('.file-item');
|
|
||||||
items.forEach((item) => {
|
|
||||||
const el = /** @type {HTMLElement} */ (item);
|
|
||||||
const fns = _listeners.get(el);
|
|
||||||
if (fns) {
|
|
||||||
el.removeEventListener('mouseenter', fns.enter);
|
|
||||||
el.removeEventListener('mouseleave', fns.leave);
|
|
||||||
_listeners.delete(el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_onLeave();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { destroy, init };
|
|
||||||
@@ -15,8 +15,8 @@ import { ResourceListComponent } from '../../components/resourceList.js';
|
|||||||
import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js';
|
import { normalizeDateBucket, sizeBucket } from '../../core/formatters.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { batchToolbar } from '../../features/files/batchToolbar.js';
|
import { batchToolbar } from '../../features/files/batchToolbar.js';
|
||||||
|
import * as itemTooltip from '../../features/itemTooltip.js';
|
||||||
import { favorites } from '../../features/library/favorites.js';
|
import { favorites } from '../../features/library/favorites.js';
|
||||||
import { ownerTooltip } from '../../features/ownerTooltip.js';
|
|
||||||
import { grants } from '../../model/grants.js';
|
import { grants } from '../../model/grants.js';
|
||||||
import { systemUsers } from '../../model/systemUsers.js';
|
import { systemUsers } from '../../model/systemUsers.js';
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ const sharedWithMeView = {
|
|||||||
batchToolbar.setActiveComponent(null);
|
batchToolbar.setActiveComponent(null);
|
||||||
|
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesList) ownerTooltip.destroy(filesList);
|
if (filesList) itemTooltip.destroy(filesList);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Internal helpers ──────────────────────────────────────────────────────
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
@@ -319,7 +319,7 @@ const sharedWithMeView = {
|
|||||||
|
|
||||||
// Wire owner tooltips after items are in the DOM
|
// Wire owner tooltips after items are in the DOM
|
||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesList) ownerTooltip.init(filesList);
|
if (filesList) itemTooltip.init(filesList);
|
||||||
|
|
||||||
// Fill the Owner column cells (idempotent: skips already-resolved rows).
|
// Fill the Owner column cells (idempotent: skips already-resolved rows).
|
||||||
await this._component?.resolveOwnerCells();
|
await this._component?.resolveOwnerCells();
|
||||||
|
|||||||
Reference in New Issue
Block a user