feat(itemToolltip): fusion pathTooltip + ownerTooltip into itemToolltip, move it in the header for more visibility
This commit is contained in:
@@ -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 { i18n } from '../../core/i18n.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' */
|
||||
|
||||
@@ -208,7 +208,7 @@ const recent = {
|
||||
}
|
||||
batchToolbar.setActiveComponent(this._component);
|
||||
this._component.render(items);
|
||||
pathTooltip.init(filesList);
|
||||
itemTooltip.init(filesList);
|
||||
}
|
||||
} catch (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 };
|
||||
Reference in New Issue
Block a user