Skip to content

Commit 1202bed

Browse files
deadlyjackAjit Kumar
andauthored
feat: plugin quicktool adapter (#2898)
* feat: plugin quicktool adapter - allow plugin to adapt quicktool - allow plugin to hook save, save event - fix icon size in file explorer * fix: Capture Swallows Later Input * fix: issues * fix: plugin based file system intialization - improve tooltip bug and visibility - UI issues * fix: restore cached files without waiting for providers Replace the startup wait with generic cache-first restoration and provider registration notifications. Preserve cached documents and keep uncached tabs idle until their filesystem becomes available. * fix: load file from cache or defer loading if file system unavailable --------- Co-authored-by: Ajit Kumar <dellevenjack@gmail>
1 parent 903d651 commit 1202bed

29 files changed

Lines changed: 3097 additions & 190 deletions

‎src/components/tabView.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function TabView({ id, disableSwipe = false }, children) {
3535
if (!activeRect.width) return;
3636
const targetLeft = activeRect.left - optionsRect.left;
3737
const targetWidth = activeRect.width;
38-
const targetTransform = `translate3d(${targetLeft}px, 0, 0)`;
38+
const targetTransform = `translateX(${targetLeft}px)`;
3939
$indicator.style.width = `${targetWidth}px`;
4040
if (document.body.classList.contains("no-animation")) {
4141
$indicator.style.transform = targetTransform;

‎src/components/tooltip/index.js‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,78 @@
11
import "./style.scss";
22
import { animate } from "motion";
3+
import { installIconTooltips } from "./longPress";
34

45
let tooltip;
56
let rafId = null;
7+
let animation;
8+
let icons;
9+
10+
export function initIconTooltips() {
11+
icons ||= installIconTooltips(document, showTooltip, hideTooltip);
12+
return icons.dismiss;
13+
}
614

715
function createTooltip() {
816
if (tooltip) return tooltip;
917

1018
tooltip = document.createElement("div");
1119
tooltip.className = "acode-tooltip";
20+
tooltip.setAttribute("role", "tooltip");
1221
document.body.appendChild(tooltip);
1322

1423
return tooltip;
1524
}
1625

17-
export function showTooltip(target, text) {
26+
export function showTooltip(target, text, description) {
1827
if (!target || !text) return;
1928

2029
const $tooltip = createTooltip();
2130

22-
$tooltip.textContent = text;
31+
animation?.stop();
32+
$tooltip.replaceChildren();
33+
const label = document.createElement("div");
34+
label.textContent = text;
35+
$tooltip.append(label);
36+
if (description) {
37+
const detail = document.createElement("div");
38+
detail.className = "acode-tooltip-description";
39+
detail.textContent = description;
40+
$tooltip.append(detail);
41+
}
42+
$tooltip.removeAttribute("aria-hidden");
2343

2444
const rect = target.getBoundingClientRect();
2545

2646
if (rafId !== null) {
2747
cancelAnimationFrame(rafId);
2848
}
2949
rafId = requestAnimationFrame(() => {
50+
const viewport = window.visualViewport;
51+
const x = viewport?.offsetLeft || 0,
52+
y = viewport?.offsetTop || 0;
53+
const visibleWidth = viewport?.width || window.innerWidth;
54+
const visibleHeight = viewport?.height || window.innerHeight;
55+
$tooltip.style.maxWidth = `${Math.max(0, visibleWidth - 16)}px`;
56+
$tooltip.style.maxHeight = `${Math.max(0, visibleHeight - 16)}px`;
3057
const width = $tooltip.offsetWidth;
3158
const height = $tooltip.offsetHeight;
3259

3360
const left = Math.max(
34-
8,
61+
x + 8,
3562
Math.min(
36-
window.innerWidth - width - 8,
63+
x + visibleWidth - width - 8,
3764
rect.left + rect.width / 2 - width / 2,
3865
),
3966
);
4067

41-
const top = Math.max(8, rect.top - height - 10);
68+
const above = rect.top - height - 10;
69+
const top = Math.max(
70+
y + 8,
71+
Math.min(
72+
y + visibleHeight - height - 8,
73+
above >= y + 8 ? above : rect.bottom + 10,
74+
),
75+
);
4276

4377
$tooltip.style.left = `${left}px`;
4478
$tooltip.style.top = `${top}px`;
@@ -48,7 +82,7 @@ export function showTooltip(target, text) {
4882
rafId = null;
4983
return;
5084
}
51-
animate(
85+
animation = animate(
5286
$tooltip,
5387
{
5488
opacity: 1,
@@ -64,6 +98,8 @@ export function showTooltip(target, text) {
6498

6599
export function hideTooltip() {
66100
if (!tooltip) return;
101+
animation?.stop();
102+
tooltip.setAttribute("aria-hidden", "true");
67103

68104
if (rafId !== null) {
69105
cancelAnimationFrame(rafId);
@@ -74,7 +110,7 @@ export function hideTooltip() {
74110
tooltip.style.transform = "translateY(5px)";
75111
return;
76112
}
77-
animate(
113+
animation = animate(
78114
tooltip,
79115
{
80116
opacity: 0,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/** One delegated listener set, including icon controls inside open ShadowRoots. */
2+
export function installIconTooltips(root, show, hide) {
3+
let press, timer, suppressed, observer;
4+
let shadowRoots = [];
5+
const pointers = new Set();
6+
const labelOf = (node) =>
7+
["data-label", "aria-label", "title"]
8+
.map((name) => node.getAttribute(name)?.trim())
9+
.find(Boolean);
10+
const targetOf = (event) => {
11+
const path = event.composedPath();
12+
// Quicktools already own their tooltip, repeat and modifier gestures.
13+
if (path.some((node) => node?.id === "quick-tools")) return;
14+
return path.find(
15+
(node) =>
16+
node?.matches?.("button, [role='button'], .icon") && labelOf(node),
17+
);
18+
};
19+
function dismiss() {
20+
clearTimeout(timer);
21+
press = undefined;
22+
observer?.disconnect();
23+
observer = undefined;
24+
for (const shadow of shadowRoots)
25+
shadow.removeEventListener("scroll", dismiss, true);
26+
shadowRoots = [];
27+
hide();
28+
}
29+
function down(event) {
30+
pointers.add(event.pointerId);
31+
dismiss();
32+
suppressed = undefined;
33+
if (pointers.size !== 1 || event.button !== 0) return;
34+
const target = targetOf(event);
35+
if (!target) return;
36+
// Element scroll events do not cross a shadow boundary.
37+
shadowRoots = event
38+
.composedPath()
39+
.filter((node) => node?.nodeType === 11 && node.host);
40+
for (const shadow of shadowRoots)
41+
shadow.addEventListener("scroll", dismiss, true);
42+
press = { target, x: event.clientX, y: event.clientY, held: false };
43+
timer = setTimeout(() => {
44+
if (!press || !target.isConnected) return;
45+
press.held = true;
46+
show(target, labelOf(target), target.dataset.description);
47+
observer = new MutationObserver(() => {
48+
if (!target.isConnected) dismiss();
49+
});
50+
// Also observe removal of the tab's host, including nested shadow hosts.
51+
for (const boundary of [root, ...shadowRoots])
52+
observer.observe(boundary, { childList: true, subtree: true });
53+
}, 500);
54+
}
55+
function move(event) {
56+
if (
57+
press &&
58+
Math.hypot(event.clientX - press.x, event.clientY - press.y) > 10
59+
)
60+
dismiss();
61+
}
62+
function up(event) {
63+
pointers.delete(event.pointerId);
64+
clearTimeout(timer);
65+
if (press?.held)
66+
suppressed = { target: press.target, until: Date.now() + 750 };
67+
press = undefined;
68+
}
69+
function cancel(event) {
70+
pointers.delete(event.pointerId);
71+
dismiss();
72+
}
73+
function click(event) {
74+
if (
75+
event.detail &&
76+
suppressed &&
77+
suppressed.target === targetOf(event) &&
78+
Date.now() < suppressed.until
79+
) {
80+
event.preventDefault();
81+
event.stopImmediatePropagation();
82+
}
83+
suppressed = undefined;
84+
}
85+
function contextmenu(event) {
86+
// A control's own menu wins over the shared tooltip.
87+
if (event.defaultPrevented) {
88+
dismiss();
89+
return;
90+
}
91+
if (press?.held && press.target === targetOf(event)) event.preventDefault();
92+
}
93+
function reset() {
94+
pointers.clear();
95+
suppressed = undefined;
96+
dismiss();
97+
}
98+
const events = {
99+
pointerdown: down,
100+
pointermove: move,
101+
pointerup: up,
102+
pointercancel: cancel,
103+
click,
104+
scroll: dismiss,
105+
keydown: dismiss,
106+
};
107+
for (const [name, handler] of Object.entries(events))
108+
root.addEventListener(name, handler, true);
109+
root.addEventListener("contextmenu", contextmenu);
110+
const viewport = root.defaultView?.visualViewport;
111+
root.defaultView?.addEventListener("blur", reset);
112+
viewport?.addEventListener("resize", dismiss);
113+
viewport?.addEventListener("scroll", dismiss);
114+
return {
115+
dismiss: reset,
116+
dispose() {
117+
reset();
118+
for (const [name, handler] of Object.entries(events))
119+
root.removeEventListener(name, handler, true);
120+
root.removeEventListener("contextmenu", contextmenu);
121+
root.defaultView?.removeEventListener("blur", reset);
122+
viewport?.removeEventListener("resize", dismiss);
123+
viewport?.removeEventListener("scroll", dismiss);
124+
},
125+
};
126+
}

‎src/components/tooltip/style.scss‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
.acode-tooltip {
22
position: fixed;
33

4-
padding: 6px 10px;
4+
padding: 8px 12px;
55

66
background: var(--popup-background-color);
77
color: var(--popup-text-color);
8-
border: 1px solid var(--popup-border-color);
9-
box-shadow: 0 0 4px var(--box-shadow-color);
8+
border: 1px solid
9+
color-mix(in srgb, var(--popup-text-color) 30%, var(--popup-background-color));
10+
box-shadow: 0 4px 12px var(--box-shadow-color);
1011
border-radius: var(--popup-border-radius);
1112

12-
font-size: 12px;
13+
font-size: 13px;
14+
font-weight: 500;
1315

14-
white-space: nowrap;
16+
white-space: normal;
17+
overflow-wrap: anywhere;
18+
overflow: hidden;
19+
box-sizing: border-box;
20+
line-height: 1.4;
1521

1622
pointer-events: none;
1723

@@ -21,4 +27,8 @@
2127

2228
z-index: 999999;
2329
}
24-
30+
.acode-tooltip-description {
31+
margin-top: 3px;
32+
font-size: 12px;
33+
font-weight: 400;
34+
}

‎src/fileSystem/index.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import internalFs from "./internalFs";
77
import Sftp from "./sftp";
88

99
const fsList = [];
10+
const registrationListeners = new Set();
11+
12+
// Internal notification; plugins keep using fsOperation.extend as before.
13+
export function onProviderRegistered(listener) {
14+
registrationListeners.add(listener);
15+
return () => registrationListeners.delete(listener);
16+
}
1017

1118
/**
1219
* @typedef {Object} Stat
@@ -61,8 +68,18 @@ export default function fsOperation(...url) {
6168
return fsList.find((fs) => fs.test(url))?.fs(url);
6269
}
6370

71+
// Check registration without constructing a transport or opening a file.
72+
export function hasProvider(url) {
73+
return fsList.some((fs) => fs.test(url));
74+
}
75+
6476
fsOperation.extend = (test, fs) => {
6577
fsList.push({ test, fs });
78+
for (const listener of registrationListeners) {
79+
Promise.resolve()
80+
.then(() => listener(test))
81+
.catch(console.error);
82+
}
6683
};
6784

6885
fsOperation.remove = (test) => {

0 commit comments

Comments
 (0)