@withl5e/l5e 0.3.3-alpha.3 → 0.3.3-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tooltip.js CHANGED
@@ -1,89 +1,93 @@
1
1
  import { autoUpdate as u, computePosition as m, offset as f, flip as h, shift as v, hide as b } from "@floating-ui/dom";
2
2
  function y() {
3
- const t = navigator.userAgent.toLowerCase(), e = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i.test(t), o = window.matchMedia("(max-width: 768px)").matches;
4
- return e || o;
3
+ const o = navigator.userAgent.toLowerCase(), e = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i.test(o), t = window.matchMedia("(max-width: 768px)").matches;
4
+ return e || t;
5
5
  }
6
6
  function w() {
7
- const t = document.querySelectorAll("[data-tooltip-id]");
8
- if (t.length === 0) return;
7
+ const o = document.querySelectorAll("[data-tooltip-id]");
8
+ if (o.length === 0) return;
9
9
  const e = y();
10
- t.forEach((o) => {
11
- o.hasAttribute("data-tooltip-initialized") || (o.setAttribute("data-tooltip-initialized", "true"), e ? o.addEventListener("click", async (n) => {
10
+ o.forEach((t) => {
11
+ t.hasAttribute("data-tooltip-initialized") || (t.setAttribute("data-tooltip-initialized", "true"), e ? t.addEventListener("click", async (n) => {
12
12
  n.preventDefault();
13
13
  const { showTooltipMobile: i } = await Promise.resolve().then(() => d);
14
- i(o);
15
- }) : o.addEventListener(
14
+ i(t);
15
+ }) : (t.addEventListener("pointerenter", () => {
16
+ t.setAttribute("data-tooltip-active", "true");
17
+ }), t.addEventListener("pointerleave", () => {
18
+ t.removeAttribute("data-tooltip-active");
19
+ }), t.addEventListener(
16
20
  "pointerenter",
17
21
  async () => {
18
22
  const { showTooltip: n } = await Promise.resolve().then(() => d);
19
- n(o);
23
+ n(t);
20
24
  },
21
25
  { passive: !0 }
22
- ));
26
+ )));
23
27
  });
24
28
  }
25
- function _() {
29
+ function H() {
26
30
  if (window.__tooltipObserver) return;
27
- const t = new MutationObserver((e) => {
28
- let o = !1;
31
+ const o = new MutationObserver((e) => {
32
+ let t = !1;
29
33
  e.forEach((n) => {
30
34
  n.type === "childList" && n.addedNodes.length > 0 && n.addedNodes.forEach((i) => {
31
35
  if (i.nodeType === Node.ELEMENT_NODE) {
32
36
  const a = i;
33
- (a.hasAttribute("data-tooltip-id") || a.querySelector("[data-tooltip-id]")) && (o = !0);
37
+ (a.hasAttribute("data-tooltip-id") || a.querySelector("[data-tooltip-id]")) && (t = !0);
34
38
  }
35
39
  });
36
- }), o && w();
40
+ }), t && w();
37
41
  });
38
- t.observe(document.body, {
42
+ o.observe(document.body, {
39
43
  childList: !0,
40
44
  subtree: !0
41
- }), window.__tooltipObserver = t;
45
+ }), window.__tooltipObserver = o;
42
46
  }
43
- const g = ({ type: t, id: e }) => `/tooltip/${t}/${e}`, L = ({ type: t, id: e }) => {
44
- const o = document.documentElement.lang;
47
+ const L = ({ type: o, id: e }) => `/tooltip/${o}/${e}`, T = ({ type: o, id: e }) => {
48
+ const t = document.documentElement.lang;
45
49
  let n = "";
46
- if (o) {
47
- const i = o.split("-")[0].toLowerCase(), l = location.pathname.match(/^\/([a-zA-Z-]+)(?:\/|$)/)?.[1];
48
- l && (l.toLowerCase() === i || l.toLowerCase() === o.toLowerCase()) && (n = `/${l}`);
50
+ if (t) {
51
+ const i = t.split("-")[0].toLowerCase(), l = location.pathname.match(/^\/([a-zA-Z-]+)(?:\/|$)/)?.[1];
52
+ l && (l.toLowerCase() === i || l.toLowerCase() === t.toLowerCase()) && (n = `/${l}`);
49
53
  }
50
- return `${n}/tooltip/${t}/${e}`;
54
+ return `${n}/tooltip/${o}/${e}`;
51
55
  };
52
- function T(t) {
53
- window.__l5eTooltipUrl = t === "auto-locale" ? L : t;
56
+ function E(o) {
57
+ window.__l5eTooltipUrl = o === "auto-locale" ? T : o;
54
58
  }
55
- function p(t) {
56
- const { tooltipId: e, tooltipType: o } = t.dataset;
57
- return (window.__l5eTooltipUrl ?? g)({ type: o ?? "", id: e ?? "", host: t });
59
+ function p(o) {
60
+ const { tooltipId: e, tooltipType: t } = o.dataset;
61
+ return (window.__l5eTooltipUrl ?? L)({ type: t ?? "", id: e ?? "", host: o });
58
62
  }
59
- async function E(t) {
60
- if (!t.matches(":hover")) return;
63
+ async function x(o) {
64
+ if (!o.hasAttribute("data-tooltip-active")) return;
61
65
  const e = document.createElement("div");
62
66
  e.className = "tp", e.innerHTML = '<div class="tp-loading">Loading...</div>', document.body.append(e);
63
- let o = null;
67
+ let t = null;
64
68
  const n = () => {
65
- o && (o(), o = null), document.body.contains(e) && e.remove();
69
+ t && (t(), t = null), document.body.contains(e) && e.remove();
66
70
  };
67
- if (t.addEventListener("pointerleave", n), !t.matches(":hover")) {
68
- t.removeEventListener("pointerleave", n), e.remove();
71
+ if (o.addEventListener("pointerleave", n), !o.hasAttribute("data-tooltip-active")) {
72
+ o.removeEventListener("pointerleave", n), e.remove();
69
73
  return;
70
74
  }
71
- o = u(t, e, () => s(t, e));
75
+ t = u(o, e, () => s(o, e));
72
76
  try {
73
- const i = await fetch(p(t), {
77
+ const i = await fetch(p(o), {
74
78
  headers: { Accept: "text/html" }
75
79
  }).then((a) => a.text());
76
80
  if (!document.body.contains(e)) return;
77
- e.innerHTML = i, await s(t, e);
81
+ e.innerHTML = i, await s(o, e);
78
82
  } catch {
79
83
  document.body.contains(e) && (e.innerHTML = '<div class="tp-error">Không thể tải tooltip</div>');
80
84
  }
81
85
  }
82
- async function x(t) {
86
+ async function g(o) {
83
87
  const e = document.createElement("div");
84
88
  e.className = "tp-overlay";
85
- const o = document.createElement("div");
86
- o.className = "tp-mobile", o.innerHTML = '<div class="tp-loading">Loading...</div>', e.append(o), document.body.append(e), document.body.style.overflow = "hidden";
89
+ const t = document.createElement("div");
90
+ t.className = "tp-mobile", t.innerHTML = '<div class="tp-loading">Loading...</div>', e.append(t), document.body.append(e), document.body.style.overflow = "hidden";
87
91
  const n = () => {
88
92
  e.remove(), document.body.style.overflow = "";
89
93
  };
@@ -91,38 +95,38 @@ async function x(t) {
91
95
  i.target === e && n();
92
96
  });
93
97
  try {
94
- const i = await fetch(p(t), {
98
+ const i = await fetch(p(o), {
95
99
  headers: { Accept: "text/html" }
96
100
  }).then((c) => c.text());
97
- o.innerHTML = "";
101
+ t.innerHTML = "";
98
102
  const a = document.createElement("button");
99
- a.className = "tp-mobile-close", a.innerHTML = "&#x2715;", a.addEventListener("click", n), o.append(a);
103
+ a.className = "tp-mobile-close", a.innerHTML = "&#x2715;", a.addEventListener("click", n), t.append(a);
100
104
  const l = document.createElement("div");
101
- l.className = "tp-mobile-content", l.innerHTML = i, o.append(l);
102
- const r = t.dataset.href;
105
+ l.className = "tp-mobile-content", l.innerHTML = i, t.append(l);
106
+ const r = o.dataset.href;
103
107
  if (r) {
104
108
  const c = document.createElement("a");
105
- c.className = "tp-mobile-link", c.href = r, c.textContent = "Xem chi tiết", o.append(c);
109
+ c.className = "tp-mobile-link", c.href = r, c.textContent = "Xem chi tiết", t.append(c);
106
110
  }
107
111
  } catch {
108
- o.innerHTML = '<div class="tp-error">Không thể tải tooltip</div>';
112
+ t.innerHTML = '<div class="tp-error">Không thể tải tooltip</div>';
109
113
  }
110
114
  }
111
115
  function M() {
112
116
  return {
113
117
  name: "centerFallback",
114
- fn({ rects: t }) {
115
- const e = window.innerHeight, o = 8;
116
- return t.floating.height <= e * 0.6 ? {} : {
117
- y: window.scrollY + Math.max(o, (e - t.floating.height) / 2),
118
- data: { centered: !0, maxHeight: e - o * 2 }
118
+ fn({ rects: o }) {
119
+ const e = window.innerHeight, t = 8;
120
+ return o.floating.height <= e * 0.6 ? {} : {
121
+ y: window.scrollY + Math.max(t, (e - o.floating.height) / 2),
122
+ data: { centered: !0, maxHeight: e - t * 2 }
119
123
  };
120
124
  }
121
125
  };
122
126
  }
123
- function s(t, e) {
124
- const n = t.dataset.tooltipPlacement ?? "left";
125
- return m(t, e, {
127
+ function s(o, e) {
128
+ const n = o.dataset.tooltipPlacement ?? "left";
129
+ return m(o, e, {
126
130
  placement: n,
127
131
  middleware: [f(6), h(), v({ padding: 5 }), M(), b()]
128
132
  }).then(({ x: i, y: a, middlewareData: l }) => {
@@ -133,15 +137,15 @@ function s(t, e) {
133
137
  }
134
138
  const d = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
135
139
  __proto__: null,
136
- configureTooltip: T,
137
- showTooltip: E,
138
- showTooltipMobile: x
140
+ configureTooltip: E,
141
+ showTooltip: x,
142
+ showTooltipMobile: g
139
143
  }, Symbol.toStringTag, { value: "Module" }));
140
144
  export {
141
- T as configureTooltip,
145
+ E as configureTooltip,
142
146
  w as initTooltips,
143
- _ as setupTooltipObserver,
144
- E as showTooltip,
145
- x as showTooltipMobile
147
+ H as setupTooltipObserver,
148
+ x as showTooltip,
149
+ g as showTooltipMobile
146
150
  };
147
151
  //# sourceMappingURL=tooltip.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.js","sources":["../src/tooltip/tooltip-loader.ts","../src/tooltip/tooltip-runtime.ts"],"sourcesContent":["// tooltip-loader.ts\n// File này chỉ làm nhiệm vụ wake up tooltip khi cần thiết\n\n// Định nghĩa kiểu cho tooltip host element\ntype TooltipHost = HTMLElement & {\n dataset: {\n tooltipId: string;\n tooltipType?: string;\n };\n};\n\n/**\n * Kiểm tra xem thiết bị hiện tại có phải là thiết bị di động hay không\n * @returns true nếu là thiết bị di động\n */\nfunction isMobileDevice(): boolean {\n // Kiểm tra User Agent\n const userAgent = navigator.userAgent.toLowerCase();\n const isMobile =\n /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i.test(userAgent);\n\n // Kiểm tra thêm kích thước màn hình (dưới 768px thường được coi là thiết bị di động)\n const isTouchScreen = window.matchMedia('(max-width: 768px)').matches;\n\n return isMobile || isTouchScreen;\n}\n\n/**\n * Hàm khởi tạo tooltip, chỉ load runtime khi cần thiết\n */\nexport function initTooltips(): void {\n const tooltipTriggers = document.querySelectorAll<TooltipHost>('[data-tooltip-id]');\n if (tooltipTriggers.length === 0) return;\n\n const mobile = isMobileDevice();\n\n tooltipTriggers.forEach((trigger) => {\n if (trigger.hasAttribute('data-tooltip-initialized')) return;\n trigger.setAttribute('data-tooltip-initialized', 'true');\n\n if (mobile) {\n trigger.addEventListener('click', async (e) => {\n e.preventDefault();\n const { showTooltipMobile } = await import('./tooltip-runtime');\n showTooltipMobile(trigger);\n });\n } else {\n trigger.addEventListener(\n 'pointerenter',\n async () => {\n const { showTooltip } = await import('./tooltip-runtime');\n showTooltip(trigger);\n },\n { passive: true },\n );\n }\n });\n}\n\n// Thêm một MutationObserver để theo dõi các phần tử DOM mới (cho React components)\nexport function setupTooltipObserver(): void {\n // Kiểm tra xem đã có observer chưa\n if (window.__tooltipObserver) return;\n\n // Observer theo dõi khi có phần tử mới được thêm vào DOM\n const observer = new MutationObserver((mutations) => {\n let shouldInit = false;\n\n mutations.forEach((mutation) => {\n if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {\n // Kiểm tra xem có phần tử tooltip nào mới được thêm vào không\n mutation.addedNodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Kiểm tra nếu phần tử có data-tooltip-id hoặc chứa phần tử con có data-tooltip-id\n if (\n element.hasAttribute('data-tooltip-id') ||\n element.querySelector('[data-tooltip-id]')\n ) {\n shouldInit = true;\n }\n }\n });\n }\n });\n\n // Chỉ gọi initTooltips nếu có phát hiện tooltip mới\n if (shouldInit) {\n initTooltips();\n }\n });\n\n // Bắt đầu theo dõi toàn bộ DOM\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n // Lưu observer vào window để tránh tạo nhiều observer\n window.__tooltipObserver = observer;\n}\n\n// Typescript declaration cho window object\ndeclare global {\n interface Window {\n __tooltipObserver?: MutationObserver;\n }\n}\n","// tooltip-runtime.ts\n// Logic đầy đủ cho tooltip, chỉ được tải khi cần thiết\n\nimport type { Middleware, Placement } from '@floating-ui/dom';\nimport { autoUpdate, computePosition, flip, hide, offset, shift } from '@floating-ui/dom';\n\ntype TooltipHost = HTMLElement & {\n dataset: {\n tooltipId: string;\n tooltipType?: string;\n tooltipPlacement?: Placement;\n href?: string;\n };\n};\n\n/** Everything the tooltip system knows about one request, for a custom strategy to build a URL from. */\nexport interface TooltipUrlContext {\n type: string;\n id: string;\n host: TooltipHost;\n}\n\nexport type TooltipUrlStrategy = (ctx: TooltipUrlContext) => string;\n\nconst defaultStrategy: TooltipUrlStrategy = ({ type, id }) => `/tooltip/${type}/${id}`;\n\n/**\n * If the page's own URL already carries a prefix matching `<html lang>` (e.g.\n * `<html lang=\"vi\">` on `/vi/...`), reuse that same prefix — for apps doing\n * URL-prefix locale routing that want each locale to be a distinct,\n * CDN-cacheable tooltip URL with zero per-page setup. A base-locale page\n * (unprefixed URL) naturally infers no prefix.\n */\nconst autoLocaleStrategy: TooltipUrlStrategy = ({ type, id }) => {\n const lang = document.documentElement.lang;\n let prefix = '';\n if (lang) {\n const primary = lang.split('-')[0].toLowerCase();\n const match = location.pathname.match(/^\\/([a-zA-Z-]+)(?:\\/|$)/);\n const segment = match?.[1];\n if (segment && (segment.toLowerCase() === primary || segment.toLowerCase() === lang.toLowerCase())) {\n prefix = `/${segment}`;\n }\n }\n return `${prefix}/tooltip/${type}/${id}`;\n};\n\ndeclare global {\n interface Window {\n // L5E's per-request bundler compiles each view's client script (and\n // client.global.ts) as independent bundles — a view that imports\n // initTooltips()/showTooltip() gets its own inlined copy of this whole\n // module, with its own module-scoped variables. A plain `let` here\n // would only ever be seen by whichever single bundle happens to define\n // it, not by the others — so this has to live on `window`, the one\n // thing every independently-bundled script actually shares.\n __l5eTooltipUrl?: TooltipUrlStrategy;\n }\n}\n\n/**\n * Replace how every tooltip's fetch URL is built. L5E is not an i18n\n * framework and most apps using tooltips don't localize URLs (or need any\n * custom scheme) at all, so the default is `/tooltip/:type/:id` unless an\n * app opts into something else — call this once at startup, e.g. in\n * `client.global.ts` so it runs regardless of which view's own client\n * script actually mounts a tooltip:\n *\n * ```ts\n * configureTooltip('auto-locale'); // infer /vi (or none) from <html lang> + URL\n * // or fully custom:\n * configureTooltip(({ type, id }) => `/api/v2/tooltips/${type}-${id}`);\n * ```\n */\nexport function configureTooltip(strategy: 'auto-locale' | TooltipUrlStrategy): void {\n window.__l5eTooltipUrl = strategy === 'auto-locale' ? autoLocaleStrategy : strategy;\n}\n\nfunction tooltipUrl(host: TooltipHost): string {\n const { tooltipId: id, tooltipType: type } = host.dataset;\n const strategy = window.__l5eTooltipUrl ?? defaultStrategy;\n return strategy({ type: type ?? '', id: id ?? '', host });\n}\n\nexport async function showTooltip(host: TooltipHost): Promise<void> {\n /*\n * Trong lúc chờ dynamic import, con trỏ có thể đã rời khỏi phần tử host.\n * Nếu vậy, việc tạo tooltip là không cần thiết và sẽ không có sự kiện\n * `pointerleave` nào được kích hoạt để ẩn tooltip.\n */\n if (!host.matches(':hover')) return;\n\n const tip = document.createElement('div');\n tip.className = 'tp';\n tip.innerHTML = '<div class=\"tp-loading\">Loading...</div>';\n document.body.append(tip);\n\n // --- Cleanup ---\n let cleanupAutoUpdate: (() => void) | null = null;\n\n const cleanup = () => {\n if (cleanupAutoUpdate) {\n cleanupAutoUpdate();\n cleanupAutoUpdate = null;\n }\n if (document.body.contains(tip)) tip.remove();\n };\n\n host.addEventListener('pointerleave', cleanup);\n\n // Con trỏ đã rời trước khi listener được gắn\n if (!host.matches(':hover')) {\n host.removeEventListener('pointerleave', cleanup);\n tip.remove();\n return;\n }\n\n // --- Position: autoUpdate thay thế manual scroll/resize ---\n cleanupAutoUpdate = autoUpdate(host, tip, () => position(host, tip));\n\n // --- Fetch nội dung từ server ---\n try {\n const html = await fetch(tooltipUrl(host), {\n headers: { Accept: 'text/html' },\n }).then((r) => r.text());\n\n if (!document.body.contains(tip)) return;\n\n tip.innerHTML = html;\n await position(host, tip);\n } catch {\n if (document.body.contains(tip)) {\n tip.innerHTML = '<div class=\"tp-error\">Không thể tải tooltip</div>';\n }\n }\n}\n\n/**\n * Mobile: hiển thị tooltip dạng fullscreen popup.\n * Nếu host có data-href, thêm nút \"Xem chi tiết\" dẫn đến link đó.\n */\nexport async function showTooltipMobile(host: TooltipHost): Promise<void> {\n const overlay = document.createElement('div');\n overlay.className = 'tp-overlay';\n\n const popup = document.createElement('div');\n popup.className = 'tp-mobile';\n popup.innerHTML = '<div class=\"tp-loading\">Loading...</div>';\n\n overlay.append(popup);\n document.body.append(overlay);\n document.body.style.overflow = 'hidden';\n\n const cleanup = () => {\n overlay.remove();\n document.body.style.overflow = '';\n };\n\n // Đóng khi bấm vào overlay (bên ngoài popup)\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) cleanup();\n });\n\n try {\n const html = await fetch(tooltipUrl(host), {\n headers: { Accept: 'text/html' },\n }).then((r) => r.text());\n\n popup.innerHTML = '';\n\n // Nút đóng\n const closeBtn = document.createElement('button');\n closeBtn.className = 'tp-mobile-close';\n closeBtn.innerHTML = '&#x2715;';\n closeBtn.addEventListener('click', cleanup);\n popup.append(closeBtn);\n\n // Nội dung\n const content = document.createElement('div');\n content.className = 'tp-mobile-content';\n content.innerHTML = html;\n popup.append(content);\n\n // Nút \"Xem chi tiết\" nếu có data-href\n const href = host.dataset.href;\n if (href) {\n const link = document.createElement('a');\n link.className = 'tp-mobile-link';\n link.href = href;\n link.textContent = 'Xem chi tiết';\n popup.append(link);\n }\n } catch {\n popup.innerHTML = '<div class=\"tp-error\">Không thể tải tooltip</div>';\n }\n}\n\n/**\n * Khi tooltip cao hơn 60% viewport, bỏ qua placement trên/dưới host\n * và căn giữa theo chiều dọc màn hình + giới hạn maxHeight để scroll.\n */\nfunction centerFallback(): Middleware {\n return {\n name: 'centerFallback',\n fn({ rects }) {\n const vh = window.innerHeight;\n const padding = 8;\n\n if (rects.floating.height <= vh * 0.6) return {};\n\n return {\n y: window.scrollY + Math.max(padding, (vh - rects.floating.height) / 2),\n data: { centered: true, maxHeight: vh - padding * 2 },\n };\n },\n };\n}\n\nfunction position(host: TooltipHost, tip: HTMLDivElement): Promise<void> {\n const preferredPlacement = host.dataset.tooltipPlacement;\n const placement = preferredPlacement ?? 'left';\n\n return computePosition(host, tip, {\n placement,\n middleware: [offset(6), flip(), shift({ padding: 5 }), centerFallback(), hide()],\n }).then(({ x, y, middlewareData }) => {\n Object.assign(tip.style, { left: `${x}px`, top: `${y}px` });\n\n tip.style.visibility = middlewareData.hide?.referenceHidden ? 'hidden' : 'visible';\n\n const center = middlewareData.centerFallback as { centered?: boolean; maxHeight?: number };\n if (center?.centered) {\n tip.style.maxHeight = `${center.maxHeight}px`;\n tip.style.overflowY = 'auto';\n } else {\n tip.style.maxHeight = '';\n tip.style.overflowY = '';\n }\n });\n}\n"],"names":["isMobileDevice","userAgent","isMobile","isTouchScreen","initTooltips","tooltipTriggers","mobile","trigger","e","showTooltipMobile","tooltipRuntime","showTooltip","setupTooltipObserver","observer","mutations","shouldInit","mutation","node","element","defaultStrategy","type","id","autoLocaleStrategy","lang","prefix","primary","segment","configureTooltip","strategy","tooltipUrl","host","tip","cleanupAutoUpdate","cleanup","autoUpdate","position","html","r","overlay","popup","closeBtn","content","href","link","centerFallback","rects","vh","padding","placement","computePosition","offset","flip","shift","hide","x","y","middlewareData","center"],"mappings":"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"}
1
+ {"version":3,"file":"tooltip.js","sources":["../src/tooltip/tooltip-loader.ts","../src/tooltip/tooltip-runtime.ts"],"sourcesContent":["// tooltip-loader.ts\n// File này chỉ làm nhiệm vụ wake up tooltip khi cần thiết\n\n// Định nghĩa kiểu cho tooltip host element\ntype TooltipHost = HTMLElement & {\n dataset: {\n tooltipId: string;\n tooltipType?: string;\n };\n};\n\n/**\n * Kiểm tra xem thiết bị hiện tại có phải là thiết bị di động hay không\n * @returns true nếu là thiết bị di động\n */\nfunction isMobileDevice(): boolean {\n // Kiểm tra User Agent\n const userAgent = navigator.userAgent.toLowerCase();\n const isMobile =\n /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i.test(userAgent);\n\n // Kiểm tra thêm kích thước màn hình (dưới 768px thường được coi là thiết bị di động)\n const isTouchScreen = window.matchMedia('(max-width: 768px)').matches;\n\n return isMobile || isTouchScreen;\n}\n\n/**\n * Hàm khởi tạo tooltip, chỉ load runtime khi cần thiết\n */\nexport function initTooltips(): void {\n const tooltipTriggers = document.querySelectorAll<TooltipHost>('[data-tooltip-id]');\n if (tooltipTriggers.length === 0) return;\n\n const mobile = isMobileDevice();\n\n tooltipTriggers.forEach((trigger) => {\n if (trigger.hasAttribute('data-tooltip-initialized')) return;\n trigger.setAttribute('data-tooltip-initialized', 'true');\n\n if (mobile) {\n trigger.addEventListener('click', async (e) => {\n e.preventDefault();\n const { showTooltipMobile } = await import('./tooltip-runtime');\n showTooltipMobile(trigger);\n });\n } else {\n // Keep the interaction state explicitly. Firefox can lose the\n // `:hover` match while the lazily-loaded runtime is being evaluated,\n // even though the pointer is still over the trigger.\n trigger.addEventListener('pointerenter', () => {\n trigger.setAttribute('data-tooltip-active', 'true');\n });\n trigger.addEventListener('pointerleave', () => {\n trigger.removeAttribute('data-tooltip-active');\n });\n trigger.addEventListener(\n 'pointerenter',\n async () => {\n const { showTooltip } = await import('./tooltip-runtime');\n showTooltip(trigger);\n },\n { passive: true },\n );\n }\n });\n}\n\n// Thêm một MutationObserver để theo dõi các phần tử DOM mới (cho React components)\nexport function setupTooltipObserver(): void {\n // Kiểm tra xem đã có observer chưa\n if (window.__tooltipObserver) return;\n\n // Observer theo dõi khi có phần tử mới được thêm vào DOM\n const observer = new MutationObserver((mutations) => {\n let shouldInit = false;\n\n mutations.forEach((mutation) => {\n if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {\n // Kiểm tra xem có phần tử tooltip nào mới được thêm vào không\n mutation.addedNodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Kiểm tra nếu phần tử có data-tooltip-id hoặc chứa phần tử con có data-tooltip-id\n if (\n element.hasAttribute('data-tooltip-id') ||\n element.querySelector('[data-tooltip-id]')\n ) {\n shouldInit = true;\n }\n }\n });\n }\n });\n\n // Chỉ gọi initTooltips nếu có phát hiện tooltip mới\n if (shouldInit) {\n initTooltips();\n }\n });\n\n // Bắt đầu theo dõi toàn bộ DOM\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n // Lưu observer vào window để tránh tạo nhiều observer\n window.__tooltipObserver = observer;\n}\n\n// Typescript declaration cho window object\ndeclare global {\n interface Window {\n __tooltipObserver?: MutationObserver;\n }\n}\n","// tooltip-runtime.ts\n// Logic đầy đủ cho tooltip, chỉ được tải khi cần thiết\n\nimport type { Middleware, Placement } from '@floating-ui/dom';\nimport { autoUpdate, computePosition, flip, hide, offset, shift } from '@floating-ui/dom';\n\ntype TooltipHost = HTMLElement & {\n dataset: {\n tooltipId: string;\n tooltipType?: string;\n tooltipPlacement?: Placement;\n href?: string;\n };\n};\n\n/** Everything the tooltip system knows about one request, for a custom strategy to build a URL from. */\nexport interface TooltipUrlContext {\n type: string;\n id: string;\n host: TooltipHost;\n}\n\nexport type TooltipUrlStrategy = (ctx: TooltipUrlContext) => string;\n\nconst defaultStrategy: TooltipUrlStrategy = ({ type, id }) => `/tooltip/${type}/${id}`;\n\n/**\n * If the page's own URL already carries a prefix matching `<html lang>` (e.g.\n * `<html lang=\"vi\">` on `/vi/...`), reuse that same prefix — for apps doing\n * URL-prefix locale routing that want each locale to be a distinct,\n * CDN-cacheable tooltip URL with zero per-page setup. A base-locale page\n * (unprefixed URL) naturally infers no prefix.\n */\nconst autoLocaleStrategy: TooltipUrlStrategy = ({ type, id }) => {\n const lang = document.documentElement.lang;\n let prefix = '';\n if (lang) {\n const primary = lang.split('-')[0].toLowerCase();\n const match = location.pathname.match(/^\\/([a-zA-Z-]+)(?:\\/|$)/);\n const segment = match?.[1];\n if (segment && (segment.toLowerCase() === primary || segment.toLowerCase() === lang.toLowerCase())) {\n prefix = `/${segment}`;\n }\n }\n return `${prefix}/tooltip/${type}/${id}`;\n};\n\ndeclare global {\n interface Window {\n // L5E's per-request bundler compiles each view's client script (and\n // client.global.ts) as independent bundles — a view that imports\n // initTooltips()/showTooltip() gets its own inlined copy of this whole\n // module, with its own module-scoped variables. A plain `let` here\n // would only ever be seen by whichever single bundle happens to define\n // it, not by the others — so this has to live on `window`, the one\n // thing every independently-bundled script actually shares.\n __l5eTooltipUrl?: TooltipUrlStrategy;\n }\n}\n\n/**\n * Replace how every tooltip's fetch URL is built. L5E is not an i18n\n * framework and most apps using tooltips don't localize URLs (or need any\n * custom scheme) at all, so the default is `/tooltip/:type/:id` unless an\n * app opts into something else — call this once at startup, e.g. in\n * `client.global.ts` so it runs regardless of which view's own client\n * script actually mounts a tooltip:\n *\n * ```ts\n * configureTooltip('auto-locale'); // infer /vi (or none) from <html lang> + URL\n * // or fully custom:\n * configureTooltip(({ type, id }) => `/api/v2/tooltips/${type}-${id}`);\n * ```\n */\nexport function configureTooltip(strategy: 'auto-locale' | TooltipUrlStrategy): void {\n window.__l5eTooltipUrl = strategy === 'auto-locale' ? autoLocaleStrategy : strategy;\n}\n\nfunction tooltipUrl(host: TooltipHost): string {\n const { tooltipId: id, tooltipType: type } = host.dataset;\n const strategy = window.__l5eTooltipUrl ?? defaultStrategy;\n return strategy({ type: type ?? '', id: id ?? '', host });\n}\n\nexport async function showTooltip(host: TooltipHost): Promise<void> {\n /*\n * Trong lúc chờ dynamic import, con trỏ có thể đã rời khỏi phần tử host.\n * Nếu vậy, việc tạo tooltip là không cần thiết và sẽ không có sự kiện\n * `pointerleave` nào được kích hoạt để ẩn tooltip.\n */\n if (!host.hasAttribute('data-tooltip-active')) return;\n\n const tip = document.createElement('div');\n tip.className = 'tp';\n tip.innerHTML = '<div class=\"tp-loading\">Loading...</div>';\n document.body.append(tip);\n\n // --- Cleanup ---\n let cleanupAutoUpdate: (() => void) | null = null;\n\n const cleanup = () => {\n if (cleanupAutoUpdate) {\n cleanupAutoUpdate();\n cleanupAutoUpdate = null;\n }\n if (document.body.contains(tip)) tip.remove();\n };\n\n host.addEventListener('pointerleave', cleanup);\n\n // Con trỏ đã rời trước khi listener được gắn\n if (!host.hasAttribute('data-tooltip-active')) {\n host.removeEventListener('pointerleave', cleanup);\n tip.remove();\n return;\n }\n\n // --- Position: autoUpdate thay thế manual scroll/resize ---\n cleanupAutoUpdate = autoUpdate(host, tip, () => position(host, tip));\n\n // --- Fetch nội dung từ server ---\n try {\n const html = await fetch(tooltipUrl(host), {\n headers: { Accept: 'text/html' },\n }).then((r) => r.text());\n\n if (!document.body.contains(tip)) return;\n\n tip.innerHTML = html;\n await position(host, tip);\n } catch {\n if (document.body.contains(tip)) {\n tip.innerHTML = '<div class=\"tp-error\">Không thể tải tooltip</div>';\n }\n }\n}\n\n/**\n * Mobile: hiển thị tooltip dạng fullscreen popup.\n * Nếu host có data-href, thêm nút \"Xem chi tiết\" dẫn đến link đó.\n */\nexport async function showTooltipMobile(host: TooltipHost): Promise<void> {\n const overlay = document.createElement('div');\n overlay.className = 'tp-overlay';\n\n const popup = document.createElement('div');\n popup.className = 'tp-mobile';\n popup.innerHTML = '<div class=\"tp-loading\">Loading...</div>';\n\n overlay.append(popup);\n document.body.append(overlay);\n document.body.style.overflow = 'hidden';\n\n const cleanup = () => {\n overlay.remove();\n document.body.style.overflow = '';\n };\n\n // Đóng khi bấm vào overlay (bên ngoài popup)\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) cleanup();\n });\n\n try {\n const html = await fetch(tooltipUrl(host), {\n headers: { Accept: 'text/html' },\n }).then((r) => r.text());\n\n popup.innerHTML = '';\n\n // Nút đóng\n const closeBtn = document.createElement('button');\n closeBtn.className = 'tp-mobile-close';\n closeBtn.innerHTML = '&#x2715;';\n closeBtn.addEventListener('click', cleanup);\n popup.append(closeBtn);\n\n // Nội dung\n const content = document.createElement('div');\n content.className = 'tp-mobile-content';\n content.innerHTML = html;\n popup.append(content);\n\n // Nút \"Xem chi tiết\" nếu có data-href\n const href = host.dataset.href;\n if (href) {\n const link = document.createElement('a');\n link.className = 'tp-mobile-link';\n link.href = href;\n link.textContent = 'Xem chi tiết';\n popup.append(link);\n }\n } catch {\n popup.innerHTML = '<div class=\"tp-error\">Không thể tải tooltip</div>';\n }\n}\n\n/**\n * Khi tooltip cao hơn 60% viewport, bỏ qua placement trên/dưới host\n * và căn giữa theo chiều dọc màn hình + giới hạn maxHeight để scroll.\n */\nfunction centerFallback(): Middleware {\n return {\n name: 'centerFallback',\n fn({ rects }) {\n const vh = window.innerHeight;\n const padding = 8;\n\n if (rects.floating.height <= vh * 0.6) return {};\n\n return {\n y: window.scrollY + Math.max(padding, (vh - rects.floating.height) / 2),\n data: { centered: true, maxHeight: vh - padding * 2 },\n };\n },\n };\n}\n\nfunction position(host: TooltipHost, tip: HTMLDivElement): Promise<void> {\n const preferredPlacement = host.dataset.tooltipPlacement;\n const placement = preferredPlacement ?? 'left';\n\n return computePosition(host, tip, {\n placement,\n middleware: [offset(6), flip(), shift({ padding: 5 }), centerFallback(), hide()],\n }).then(({ x, y, middlewareData }) => {\n Object.assign(tip.style, { left: `${x}px`, top: `${y}px` });\n\n tip.style.visibility = middlewareData.hide?.referenceHidden ? 'hidden' : 'visible';\n\n const center = middlewareData.centerFallback as { centered?: boolean; maxHeight?: number };\n if (center?.centered) {\n tip.style.maxHeight = `${center.maxHeight}px`;\n tip.style.overflowY = 'auto';\n } else {\n tip.style.maxHeight = '';\n tip.style.overflowY = '';\n }\n });\n}\n"],"names":["isMobileDevice","userAgent","isMobile","isTouchScreen","initTooltips","tooltipTriggers","mobile","trigger","e","showTooltipMobile","tooltipRuntime","showTooltip","setupTooltipObserver","observer","mutations","shouldInit","mutation","node","element","defaultStrategy","type","id","autoLocaleStrategy","lang","prefix","primary","segment","configureTooltip","strategy","tooltipUrl","host","tip","cleanupAutoUpdate","cleanup","autoUpdate","position","html","r","overlay","popup","closeBtn","content","href","link","centerFallback","rects","vh","padding","placement","computePosition","offset","flip","shift","hide","x","y","middlewareData","center"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withl5e/l5e",
3
- "version": "0.3.3-alpha.3",
3
+ "version": "0.3.3-alpha.4",
4
4
  "description": "HTML-first SSR MPA framework with loaders, middleware, islands, actions, swap, SEO and cache controls.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,15 @@ export function initTooltips(): void {
45
45
  showTooltipMobile(trigger);
46
46
  });
47
47
  } else {
48
+ // Keep the interaction state explicitly. Firefox can lose the
49
+ // `:hover` match while the lazily-loaded runtime is being evaluated,
50
+ // even though the pointer is still over the trigger.
51
+ trigger.addEventListener('pointerenter', () => {
52
+ trigger.setAttribute('data-tooltip-active', 'true');
53
+ });
54
+ trigger.addEventListener('pointerleave', () => {
55
+ trigger.removeAttribute('data-tooltip-active');
56
+ });
48
57
  trigger.addEventListener(
49
58
  'pointerenter',
50
59
  async () => {
@@ -88,7 +88,7 @@ export async function showTooltip(host: TooltipHost): Promise<void> {
88
88
  * Nếu vậy, việc tạo tooltip là không cần thiết và sẽ không có sự kiện
89
89
  * `pointerleave` nào được kích hoạt để ẩn tooltip.
90
90
  */
91
- if (!host.matches(':hover')) return;
91
+ if (!host.hasAttribute('data-tooltip-active')) return;
92
92
 
93
93
  const tip = document.createElement('div');
94
94
  tip.className = 'tp';
@@ -109,7 +109,7 @@ export async function showTooltip(host: TooltipHost): Promise<void> {
109
109
  host.addEventListener('pointerleave', cleanup);
110
110
 
111
111
  // Con trỏ đã rời trước khi listener được gắn
112
- if (!host.matches(':hover')) {
112
+ if (!host.hasAttribute('data-tooltip-active')) {
113
113
  host.removeEventListener('pointerleave', cleanup);
114
114
  tip.remove();
115
115
  return;