@brightspot/ui 1.4.0 → 1.4.1
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/custom-elements.json +144 -144
- package/dist/storybook/assets/{Avatar.stories-DrhezTR1.js → Avatar.stories-QxWs-YfX.js} +1 -1
- package/dist/storybook/assets/{AvatarGroup.stories-DrlxT-mF.js → AvatarGroup.stories-Cy_Bvn7E.js} +1 -1
- package/dist/storybook/assets/{Badge.stories-DtJcBfOR.js → Badge.stories-BpaApWbR.js} +1 -1
- package/dist/storybook/assets/{Button.stories-BKUfLgSY.js → Button.stories-C5h2usmd.js} +1 -1
- package/dist/storybook/assets/{CircularProgress.stories-dpmD-XJT.js → CircularProgress.stories-DlPOiGja.js} +1 -1
- package/dist/storybook/assets/{ClipboardMixin.stories-C0pnQ7BY.js → ClipboardMixin.stories-Bb45-UOM.js} +1 -1
- package/dist/storybook/assets/Color-6BZIO3FS-CcgGYVAo.js +1 -0
- package/dist/storybook/assets/{Colors.stories-bKK25qgF.js → Colors.stories-DP2JKWUJ.js} +1 -1
- package/dist/storybook/assets/{ComponentStatesMixin-C2HZ9ZFb.js → ComponentStatesMixin-B7ci0thi.js} +1 -1
- package/dist/storybook/assets/{ComponentStatesMixin.stories-9mRp2zuB.js → ComponentStatesMixin.stories-CyQ2aSTu.js} +1 -1
- package/dist/storybook/assets/{CopyToClipboard.stories-BW3oaT1i.js → CopyToClipboard.stories-DR7pckeV.js} +1 -1
- package/dist/storybook/assets/{Debounce.stories-BXx3CKvQ.js → Debounce.stories-hkqyvqmg.js} +3 -3
- package/dist/storybook/assets/DocsRenderer-LL677BLK-Dtw9GMer.js +758 -0
- package/dist/storybook/assets/{Events.stories-PBeiuWQn.js → Events.stories-BAgDzdyl.js} +1 -1
- package/dist/storybook/assets/{Heading.stories-Djkl0MoC.js → Heading.stories-CN_fPsRf.js} +1 -1
- package/dist/storybook/assets/{Icon.stories-Cam1fyud.js → Icon.stories-CSx_2K8V.js} +1 -1
- package/dist/storybook/assets/{LinearProgress.stories-BDNoYIJu.js → LinearProgress.stories-In48DY2g.js} +1 -1
- package/dist/storybook/assets/{Rtc.stories-BrTAIAi1.js → Rtc.stories-19d7WXe4.js} +3 -3
- package/dist/storybook/assets/{ScrollShadow.stories-DHcKhkag.js → ScrollShadow.stories-BFjracVd.js} +1 -1
- package/dist/storybook/assets/{Throttle.stories-cSYT_BXu.js → Throttle.stories-DD6ydiVq.js} +8 -8
- package/dist/storybook/assets/WithTooltip-65CFNBJE-Be1dKqOF.js +9 -0
- package/dist/storybook/assets/formatter-EIJCOSYU-anC2P5HS.js +1 -0
- package/dist/storybook/assets/{iframe-BMxUFmpF.css → iframe-B4njXYq6.css} +1 -1
- package/dist/storybook/assets/iframe-Bl9oHz5c.js +1061 -0
- package/dist/storybook/assets/index-Cn5E5A3G.js +1 -0
- package/dist/storybook/assets/onFind-DqriYjEB.js +1 -0
- package/dist/storybook/assets/onFind.stories-BMDLUk0l.js +1069 -0
- package/dist/storybook/assets/{onRemove.stories-C7W9KyRr.js → onRemove.stories-C3FcxtYh.js} +3 -3
- package/dist/storybook/assets/{onVisible.stories-CIl6R0q4.js → onVisible.stories-B8Zyu0Th.js} +10 -10
- package/dist/storybook/assets/syntaxhighlighter-ED5Y7EFY-BfTKsIVL.js +6 -0
- package/dist/storybook/iframe.html +57 -39
- package/dist/storybook/index.html +11 -4
- package/dist/storybook/index.json +1 -1
- package/dist/storybook/project.json +1 -1
- package/dist/storybook/sb-addons/docs-1/manager-bundle.js +1 -1
- package/dist/storybook/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +112 -290
- package/dist/storybook/sb-addons/vitest-2/manager-bundle.js +3 -0
- package/dist/storybook/sb-manager/globals-runtime.js +60754 -66346
- package/dist/storybook/sb-manager/globals.js +2 -3
- package/dist/storybook/sb-manager/manager-stores.js +23 -0
- package/dist/storybook/sb-manager/runtime.js +12983 -11699
- package/dist/storybook/vite-inject-mocker-entry.js +2 -2
- package/dist/util/onFind.d.ts +1 -0
- package/dist/util/onFind.d.ts.map +1 -1
- package/dist/util/onFind.js +73 -48
- package/dist/util/onFind.js.map +1 -1
- package/dist/util/onVisible.d.ts.map +1 -1
- package/dist/util/onVisible.js +13 -2
- package/dist/util/onVisible.js.map +1 -1
- package/package.json +12 -5
- package/dist/storybook/assets/Color-64QXVMR3-Dnd9S2a1.js +0 -1
- package/dist/storybook/assets/WithTooltip-SK46ZJ2J-Df0E-KJO.js +0 -825
- package/dist/storybook/assets/formatter-OMEEQ6HG-DFa_WTfb.js +0 -1
- package/dist/storybook/assets/iframe-lTczLWsL.js +0 -1064
- package/dist/storybook/assets/index-yMswRDPh.js +0 -1
- package/dist/storybook/assets/onFind-C6olvKHR.js +0 -1
- package/dist/storybook/assets/onFind.stories-DfW54CDE.js +0 -284
- package/dist/storybook/assets/syntaxhighlighter-CAVLW7PM-DoI0ixeu.js +0 -6
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import{x as h}from"./iframe-Bl9oHz5c.js";import{l as P,o as b}from"./onFind-DqriYjEB.js";import"./preload-helper-PPVm8Dsz.js";import"./_commonjsHelpers-CqkleIqs.js";const F=Symbol.for("brightspot.onFind");let y;function v(){return y||(y=globalThis[F],y||(y={calledIndex:0,callbacks:[],blacklist:[],triggerPaused:!1,triggerOnResume:!1,triggerAll:!1,triggerElements:[],triggerFrame:0,initialized:!1},globalThis[F]=y),y)}class O{#n;#e;#t;#a;#s;constructor(t,e,n){this.#n=t,this.#e=`data-ofc${v().calledIndex++}`,Array.isArray(e)||(e=[e]),this.#t=e.map(e.some(o=>o.indexOf(",")>-1)?o=>o:o=>`${o}:not([${this.#e}])`).join(","),this.#a=n;const a=e.map(o=>o.trim()).filter(o=>o.indexOf(" ")<0&&o.indexOf(",")<0);a.length===e.length&&(this.#s=a)}addTriggerElements(t,e){if(this.#n.contains(e)){e.matches(this.#t)&&t.push(e);for(const n of e.querySelectorAll(this.#t))t.push(n)}}trigger(t){if(document.readyState!=="loading")if(this.#s&&Array.isArray(t))for(const e of t)this.#s.some(n=>e.matches(n))&&this.#o(e);else for(const e of this.#n.querySelectorAll(this.#t))this.#o(e)}#o(t){if(!H(t)&&!t.hasAttribute(this.#e)){t.setAttribute(this.#e,"");try{this.#a(t)}catch(e){P.error("Failed callback!",t,e)}}}}function H(s){if(!(s instanceof Element))return!1;const{blacklist:t}=v();return t.length>0&&t.some(e=>s.closest(e)!==null)}const $=Array.prototype.every,S=s=>s.nodeType===Node.TEXT_NODE;function C(s){const t=v();if(t.triggerPaused){t.triggerOnResume=!0;return}const e=Array.isArray(s),n=[];if(e){for(const a of s){const o=a.target;if(!H(o))switch(a.type){case"attributes":a.oldValue!==o.getAttribute(a.attributeName)&&n.push(a);break;case"childList":(!$.call(a.addedNodes,S)||!$.call(a.removedNodes,S))&&n.push(a);break}}if(n.length===0)return}if(n.length>500&&(t.triggerAll=!0),!t.triggerAll)if(e)for(const a of n){const o=a.target;switch(a.type){case"attributes":t.triggerElements.push(o);break;case"childList":for(const r of t.callbacks)r.addTriggerElements(t.triggerElements,o);break}}else t.triggerAll=!0;(t.triggerAll||t.triggerElements.length>0)&&!t.triggerFrame&&(t.triggerFrame=window.requestAnimationFrame(()=>{const a=t.triggerAll?void 0:t.triggerElements;t.triggerAll=!1,t.triggerElements=[],t.triggerFrame=0,t.callbacks.forEach(o=>o.trigger(a))}))}function z(){C(),new MutationObserver(C).observe(document,{attributes:!0,attributeFilter:["class","data-bsp-autosubmit","data-chart-type","data-code-type","data-internal-name","data-tab","name","rel","target"],attributeOldValue:!0,childList:!0,subtree:!0})}function D(){const s=v();s.initialized||(s.initialized=!0,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",z):z())}function m(s,t,e){D();let n,a;typeof e<"u"?(n=s,a=t):(n=document,a=s,e=t);const o=new O(n,a,e);o.trigger(),v().callbacks.push(o)}m.triggerCallbacks=C;m.pause=()=>{v().triggerPaused=!0};m.resume=()=>{const s=v();s.triggerPaused=!1,s.triggerOnResume&&(s.triggerOnResume=!1,setTimeout(C,100))};m.ignore=(...s)=>{const{blacklist:t}=v();for(const e of s){try{document.querySelector(e)}catch{continue}t.includes(e)||t.push(e)}};const{expect:d,userEvent:c,waitFor:u,within:k}=__STORYBOOK_MODULE_TEST__,U={title:"Utilities/onFind",tags:["autodocs"],parameters:{docs:{subtitle:"The `onFind` utility observes DOM mutations and executes callbacks when elements matching specified selectors appear. Uses MutationObserver internally to efficiently track element additions and attribute changes."},controls:{expanded:!0}},argTypes:{selector:{control:{type:"text"},description:"CSS selector to watch for"}},args:{selector:".dynamic-item"}},f={render:s=>{const t=`onfind-${Math.random().toString(36).substring(2,9)}`,e=`.dynamic-item-${t}`;let n=0,a=0;const o=()=>{const i=document.getElementById(`${t}-items-count`),l=document.getElementById(`${t}-found-count`);i&&(i.textContent=String(n)),l&&(l.textContent=String(a))};return b(e,i=>{a++,o(),i.classList.add("ring-2","ring-success-500")}),h`
|
|
2
|
+
<div class="space-y-4">
|
|
3
|
+
<div class="text-base">
|
|
4
|
+
<p class="mb-2">
|
|
5
|
+
Click "Add Item" to dynamically create elements. The onFind observer detects them and highlights them with a
|
|
6
|
+
green border.
|
|
7
|
+
</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="flex gap-2">
|
|
11
|
+
<button
|
|
12
|
+
data-testid="add-item"
|
|
13
|
+
@click=${()=>{n++;const i=document.getElementById(`${t}-container`);if(i){const l=document.createElement("div");l.className=`dynamic-item-${t} rounded border bg-white p-4`,l.textContent=`Item ${n}`,i.appendChild(l)}o()}}
|
|
14
|
+
class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
|
|
15
|
+
>
|
|
16
|
+
Add Item
|
|
17
|
+
</button>
|
|
18
|
+
<button
|
|
19
|
+
data-testid="clear-items"
|
|
20
|
+
@click=${()=>{n=0,a=0;const i=document.getElementById(`${t}-container`);i&&(i.innerHTML=""),o()}}
|
|
21
|
+
class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300"
|
|
22
|
+
>
|
|
23
|
+
Clear All
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div id="${t}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
28
|
+
<div class="text-sm text-gray-500">Items will appear here...</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="flex justify-around">
|
|
32
|
+
<div>
|
|
33
|
+
<div class="text-xs text-gray-500">Items created</div>
|
|
34
|
+
<div data-testid="items-count" id="${t}-items-count" class="text-2xl font-bold text-gray-900">
|
|
35
|
+
0
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
40
|
+
<div data-testid="found-count" id="${t}-found-count" class="text-primary-600 text-2xl font-bold">
|
|
41
|
+
0
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
`},parameters:{docs:{description:{story:"Interactive example showing onFind detecting dynamically added elements. Elements are automatically highlighted when detected."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Detects dynamically added element",async()=>{await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("1"))}),await t("Detects multiple elements",async()=>{await c.click(e.getByTestId("add-item")),await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("3"))}),await t("Clear resets counts",async()=>{await c.click(e.getByTestId("clear-items")),d(e.getByTestId("items-count")).toHaveTextContent("0"),d(e.getByTestId("found-count")).toHaveTextContent("0")}),await t("Detection resumes after clear",async()=>{await c.click(e.getByTestId("add-item")),await u(()=>d(e.getByTestId("found-count")).toHaveTextContent("1"))})}},B={render:()=>{const s="onfind-ignore-demo",t=".ignore-demo-item",e="ignore-demo-zone";let n=0,a=0;const o=()=>{const i=document.getElementById(`${s}-found-count`);i&&(i.textContent=String(n))};return b.ignore(`.${e}`),b(t,i=>{n++,o(),i.classList.add("ring-2","ring-success-500")}),h`
|
|
47
|
+
<div class="space-y-4">
|
|
48
|
+
<p class="text-base">
|
|
49
|
+
Elements added inside the <strong>ignored zone</strong> are skipped by onFind callbacks. Elements added
|
|
50
|
+
outside are detected normally.
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div class="flex gap-2">
|
|
54
|
+
<button
|
|
55
|
+
data-testid="add-outside"
|
|
56
|
+
@click=${()=>{a++;const i=document.getElementById(`${s}-outside`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Outside ${a}`,i.appendChild(l)}}}
|
|
57
|
+
class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
|
|
58
|
+
>
|
|
59
|
+
Add Outside
|
|
60
|
+
</button>
|
|
61
|
+
<button data-testid="add-inside" @click=${()=>{a++;const i=document.getElementById(`${s}-inside`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Inside ${a}`,i.appendChild(l)}}} class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300">
|
|
62
|
+
Add Inside Ignored
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="grid grid-cols-2 gap-4">
|
|
67
|
+
<div>
|
|
68
|
+
<div class="mb-1 text-xs font-semibold text-gray-500">Normal zone</div>
|
|
69
|
+
<div id="${s}-outside" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
70
|
+
<div class="text-sm text-gray-400">Items detected here</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<div class="mb-1 text-xs font-semibold text-gray-500">Ignored zone</div>
|
|
75
|
+
<div
|
|
76
|
+
id="${s}-inside"
|
|
77
|
+
class="${e} min-h-32 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
|
|
78
|
+
>
|
|
79
|
+
<div class="text-sm text-gray-400">Items ignored here</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div>
|
|
85
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
86
|
+
<div
|
|
87
|
+
data-testid="ignore-found-count"
|
|
88
|
+
id="${s}-found-count"
|
|
89
|
+
class="text-primary-600 text-2xl font-bold"
|
|
90
|
+
>
|
|
91
|
+
0
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
`},parameters:{docs:{description:{story:"Demonstrates `onFind.ignore()` — elements inside ignored containers are excluded from callback processing. Useful for skipping rich-text editors or other zones where DOM mutations should not trigger component initialization."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Detects outside ignored zone",async()=>{await c.click(e.getByTestId("add-outside")),await u(()=>d(e.getByTestId("ignore-found-count")).toHaveTextContent("1"))}),await t("Skips inside ignored zone",async()=>{await c.click(e.getByTestId("add-inside")),await new Promise(n=>requestAnimationFrame(()=>requestAnimationFrame(n))),d(e.getByTestId("ignore-found-count")).toHaveTextContent("1")}),await t("Continues detecting outside",async()=>{await c.click(e.getByTestId("add-outside")),await u(()=>d(e.getByTestId("ignore-found-count")).toHaveTextContent("2"))})}},w={render:()=>{const s="onfind-pause-demo",t=".pause-demo-item";let e=0,n=0;const a=()=>{const i=document.getElementById(`${s}-found-count`);i&&(i.textContent=String(e))};b(t,i=>{e++,a(),i.classList.add("ring-2","ring-success-500")});const o=()=>{n++;const i=document.getElementById(`${s}-container`);if(i){const l=document.createElement("div");l.className=`${t.slice(1)} rounded border bg-white p-4`,l.textContent=`Item ${n}`,i.appendChild(l)}};let r=!1;return h`
|
|
96
|
+
<div class="space-y-4">
|
|
97
|
+
<p class="text-base">
|
|
98
|
+
Use <code>onFind.pause()</code> to temporarily stop detection and <code>onFind.resume()</code> to restart it.
|
|
99
|
+
Elements added while paused are detected on resume.
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
<div class="flex gap-2">
|
|
103
|
+
<button data-testid="pause-add" @click=${o} class="btu-button btu-button-primary btu-button-sm">
|
|
104
|
+
Add Item
|
|
105
|
+
</button>
|
|
106
|
+
<button data-testid="toggle-pause" @click=${()=>{r=!r,r?b.pause():b.resume();const i=document.querySelector('[data-testid="toggle-pause"]');i&&(i.textContent=r?"Resume":"Pause",i.className=r?"btu-button btu-button-success btu-button-sm":"btu-button btu-button-warning btu-button-sm")}} class="btu-button btu-button-warning btu-button-sm">
|
|
107
|
+
Pause
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div id="${s}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
112
|
+
<div class="text-sm text-gray-500">Items will appear here...</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
117
|
+
<div
|
|
118
|
+
data-testid="pause-found-count"
|
|
119
|
+
id="${s}-found-count"
|
|
120
|
+
class="text-primary-600 text-2xl font-bold"
|
|
121
|
+
>
|
|
122
|
+
0
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`},parameters:{docs:{description:{story:"Demonstrates `onFind.pause()` and `onFind.resume()`. While paused, DOM mutations are queued and processed when resumed."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Normal detection before pause",async()=>{await c.click(e.getByTestId("pause-add")),await u(()=>d(e.getByTestId("pause-found-count")).toHaveTextContent("1"))}),await t("Pause stops detection",async()=>{await c.click(e.getByTestId("toggle-pause")),await c.click(e.getByTestId("pause-add")),await new Promise(n=>setTimeout(n,200)),d(e.getByTestId("pause-found-count")).toHaveTextContent("1")}),await t("Resume triggers pending detection",async()=>{await c.click(e.getByTestId("toggle-pause")),await u(()=>d(e.getByTestId("pause-found-count")).toHaveTextContent("2"),{timeout:2e3})})}},I={render:()=>{const s=b,t=".singleton-item-a",e=".singleton-item-b";let n=0,a=0;const o=()=>{const g=document.getElementById("singleton-count-a"),x=document.getElementById("singleton-count-b");g&&(g.textContent=String(n)),x&&(x.textContent=String(a))};s.ignore(".singleton-ignored-zone"),s(t,g=>{n++,o(),g.classList.add("ring-2","ring-blue-500")}),m(e,g=>{a++,o(),g.classList.add("ring-2","ring-purple-500")});const r=(g,x,M)=>{const A=document.getElementById(M);if(A){const E=document.createElement("div");E.className=`${g.slice(1)} rounded border bg-white p-4`,E.textContent=x,A.appendChild(E)}};let p=!1;return h`
|
|
127
|
+
<div class="space-y-4">
|
|
128
|
+
<p class="text-base">
|
|
129
|
+
Two separate ES module instances of <code>onFind</code> (each with its own <code>_state</code> cache) share
|
|
130
|
+
one observer, ignore list, and pause/resume state via <code>globalThis[Symbol.for()]</code>.
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
<div class="flex items-center gap-4">
|
|
134
|
+
<button
|
|
135
|
+
data-testid="singleton-toggle-pause"
|
|
136
|
+
@click=${()=>{p=!p,p?s.pause():s.resume();const g=document.querySelector('[data-testid="singleton-toggle-pause"]');g&&(g.textContent=p?"Resume":"Pause",g.className=p?"btu-button btu-button-success btu-button-sm":"btu-button btu-button-warning btu-button-sm")}}
|
|
137
|
+
class="btu-button btu-button-warning btu-button-sm"
|
|
138
|
+
>
|
|
139
|
+
Pause
|
|
140
|
+
</button>
|
|
141
|
+
<div class="flex items-center gap-1 text-sm text-blue-600">
|
|
142
|
+
Bundle A:
|
|
143
|
+
<span data-testid="singleton-count-a" id="singleton-count-a" class="font-bold">0</span>
|
|
144
|
+
items found
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex items-center gap-1 text-sm text-purple-600">
|
|
147
|
+
Bundle B:
|
|
148
|
+
<span data-testid="singleton-count-b" id="singleton-count-b" class="font-bold">0</span>
|
|
149
|
+
items found
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="grid grid-cols-3 gap-4">
|
|
154
|
+
<div>
|
|
155
|
+
<div class="mb-2 text-xs font-semibold text-blue-600">Bundle A zone</div>
|
|
156
|
+
<div class="mb-2 flex gap-1">
|
|
157
|
+
<button
|
|
158
|
+
data-testid="singleton-add-a"
|
|
159
|
+
@click=${()=>r(t,"Bundle A item","singleton-zone-a")}
|
|
160
|
+
class="btu-button btu-button-primary btu-button-sm"
|
|
161
|
+
>
|
|
162
|
+
Add A
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
data-testid="singleton-zone-a-add-b"
|
|
166
|
+
@click=${()=>r(e,"Bundle B item","singleton-zone-a")}
|
|
167
|
+
class="btu-button btu-button-purple btu-button-sm"
|
|
168
|
+
>
|
|
169
|
+
Add B
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div id="singleton-zone-a" class="min-h-24 space-y-2 rounded border-2 border-blue-300 bg-blue-50 p-4">
|
|
173
|
+
<div class="text-sm text-gray-400">A items here</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<div class="mb-2 text-xs font-semibold text-purple-600">Bundle B zone</div>
|
|
178
|
+
<div class="mb-2 flex gap-1">
|
|
179
|
+
<button
|
|
180
|
+
data-testid="singleton-zone-b-add-a"
|
|
181
|
+
@click=${()=>r(t,"Bundle A item","singleton-zone-b")}
|
|
182
|
+
class="btu-button btu-button-primary btu-button-sm"
|
|
183
|
+
>
|
|
184
|
+
Add A
|
|
185
|
+
</button>
|
|
186
|
+
<button
|
|
187
|
+
data-testid="singleton-add-b"
|
|
188
|
+
@click=${()=>r(e,"Bundle B item","singleton-zone-b")}
|
|
189
|
+
class="btu-button btu-button-purple btu-button-sm"
|
|
190
|
+
>
|
|
191
|
+
Add B
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div id="singleton-zone-b" class="min-h-24 space-y-2 rounded border-2 border-purple-300 bg-purple-50 p-4">
|
|
195
|
+
<div class="text-sm text-gray-400">B items here</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div>
|
|
199
|
+
<div class="mb-2 text-xs font-semibold text-red-600">Ignored zone</div>
|
|
200
|
+
<div class="mb-2 flex gap-1">
|
|
201
|
+
<button
|
|
202
|
+
data-testid="singleton-add-ignored"
|
|
203
|
+
@click=${()=>{r(t,"Ignored A","singleton-ignored"),r(e,"Ignored B","singleton-ignored")}}
|
|
204
|
+
class="btu-button btu-button-gray btu-button-sm"
|
|
205
|
+
>
|
|
206
|
+
Add A & B
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
<div
|
|
210
|
+
id="singleton-ignored"
|
|
211
|
+
class="singleton-ignored-zone min-h-24 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
|
|
212
|
+
>
|
|
213
|
+
<div class="text-sm text-gray-400">Items ignored here</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
`},parameters:{docs:{description:{story:"Two independent module instances of `onFind` (simulating separate bundles) share a single MutationObserver, ignore list, and pause/resume state through the `globalThis[Symbol.for()]` singleton pattern."}}},play:async({canvasElement:s,step:t})=>{const e=k(s);await t("Shared observer: Bundle A detects A items, Bundle B detects B items",async()=>{await c.click(e.getByTestId("singleton-add-a")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("1")),await c.click(e.getByTestId("singleton-add-b")),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("1"))}),await t("Cross-zone detection fires correct callback",async()=>{await c.click(e.getByTestId("singleton-zone-a-add-b")),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("2")),d(e.getByTestId("singleton-count-a")).toHaveTextContent("1"),await c.click(e.getByTestId("singleton-zone-b-add-a")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("2")),d(e.getByTestId("singleton-count-b")).toHaveTextContent("2")}),await t("Shared ignore list: Bundle A ignore() blocks Bundle B callbacks",async()=>{const n=e.getByTestId("singleton-count-a").textContent,a=e.getByTestId("singleton-count-b").textContent;await c.click(e.getByTestId("singleton-add-ignored")),await new Promise(o=>requestAnimationFrame(()=>requestAnimationFrame(o))),d(e.getByTestId("singleton-count-a")).toHaveTextContent(n),d(e.getByTestId("singleton-count-b")).toHaveTextContent(a)}),await t("Ignore lists are merged and deduped across bundles",async()=>{const n=globalThis[Symbol.for("brightspot.onFind")],a=n.blacklist.length;m.ignore(".singleton-ignored-zone"),d(n.blacklist.length).toBe(a),m.ignore(".bundle-b-ignored"),d(n.blacklist).toContain(".singleton-ignored-zone"),d(n.blacklist).toContain(".bundle-b-ignored"),d(n.blacklist.length).toBe(a+1)}),await t("Shared pause/resume: Bundle A pause() stops Bundle B detection",async()=>{await c.click(e.getByTestId("singleton-toggle-pause")),await c.click(e.getByTestId("singleton-add-a")),await c.click(e.getByTestId("singleton-add-b")),await new Promise(n=>setTimeout(n,200)),d(e.getByTestId("singleton-count-a")).toHaveTextContent("2"),d(e.getByTestId("singleton-count-b")).toHaveTextContent("2"),await c.click(e.getByTestId("singleton-toggle-pause")),await u(()=>d(e.getByTestId("singleton-count-a")).toHaveTextContent("3"),{timeout:2e3}),await u(()=>d(e.getByTestId("singleton-count-b")).toHaveTextContent("3"),{timeout:2e3})}),await t("No index collision on data-ofc attributes",async()=>{const n=s.querySelectorAll(".singleton-item-a, .singleton-item-b"),a=new Set;n.forEach(o=>{o.getAttributeNames().filter(r=>r.startsWith("data-ofc")).forEach(r=>a.add(r))}),d(a.size).toBeGreaterThanOrEqual(2)}),await t("Singleton state: both bundles share the same globalThis object",async()=>{const n=globalThis[Symbol.for("brightspot.onFind")];d(n).toBeDefined(),d(n.callbacks).toBeDefined(),d(Array.isArray(n.callbacks)).toBe(!0),d(n.initialized).toBe(!0),d(b.pause).not.toBe(m.pause),d(b.resume).not.toBe(m.resume)})}},T={render:()=>h`
|
|
219
|
+
<div class="space-y-4 text-sm">
|
|
220
|
+
<div>
|
|
221
|
+
<h3 class="mb-2 font-bold">Basic Usage</h3>
|
|
222
|
+
<pre
|
|
223
|
+
class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
|
|
224
|
+
><code>import onFind from '@brightspot/ui/util/onFind.js'
|
|
225
|
+
|
|
226
|
+
// Watch for elements globally
|
|
227
|
+
onFind('.my-component', (element) => {
|
|
228
|
+
console.log('Found element:', element)
|
|
229
|
+
// Initialize component
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Watch within a specific root element
|
|
233
|
+
const container = document.querySelector('#container')
|
|
234
|
+
onFind(container, '.my-widget', (element) => {
|
|
235
|
+
// Initialize widget
|
|
236
|
+
})</code></pre>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div>
|
|
240
|
+
<h3 class="mb-2 font-bold">Multiple Selectors</h3>
|
|
241
|
+
<pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Watch for multiple selectors at once
|
|
242
|
+
onFind(['.button', '.input', '.select'], (element) => {
|
|
243
|
+
if (element.matches('.button')) {
|
|
244
|
+
// Handle button
|
|
245
|
+
} else if (element.matches('.input')) {
|
|
246
|
+
// Handle input
|
|
247
|
+
}
|
|
248
|
+
})</code></pre>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div>
|
|
252
|
+
<h3 class="mb-2 font-bold">Pause and Resume</h3>
|
|
253
|
+
<pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Pause observations temporarily
|
|
254
|
+
onFind.pause()
|
|
255
|
+
|
|
256
|
+
// Make bulk DOM changes without triggering callbacks
|
|
257
|
+
// ...
|
|
258
|
+
|
|
259
|
+
// Resume observations
|
|
260
|
+
onFind.resume()</code></pre>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div>
|
|
264
|
+
<h3 class="mb-2 font-bold">Ignore Selectors</h3>
|
|
265
|
+
<pre
|
|
266
|
+
class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
|
|
267
|
+
><code>// Exclude elements inside matching containers from all callbacks.
|
|
268
|
+
// Calls are additive and shared across bundles.
|
|
269
|
+
onFind.ignore('.ProseMirror', '.RichTextEditor')
|
|
270
|
+
|
|
271
|
+
// Elements inside ignored containers will not trigger callbacks,
|
|
272
|
+
// even if they match a registered selector.</code></pre>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div>
|
|
276
|
+
<h3 class="mb-2 font-bold">Common Use Cases</h3>
|
|
277
|
+
<ul class="list-inside list-disc space-y-1 text-gray-700">
|
|
278
|
+
<li>Initialize components when they appear in the DOM</li>
|
|
279
|
+
<li>Attach event listeners to dynamically created elements</li>
|
|
280
|
+
<li>Enhance server-rendered content progressively</li>
|
|
281
|
+
<li>Detect and respond to attribute changes</li>
|
|
282
|
+
<li>Apply transformations to newly added content</li>
|
|
283
|
+
</ul>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div>
|
|
287
|
+
<h3 class="mb-2 font-bold">Parameters</h3>
|
|
288
|
+
<ul class="space-y-2">
|
|
289
|
+
<li>
|
|
290
|
+
<code class="rounded bg-gray-200 px-1">root</code> (ParentNode, optional): Root element to observe. Defaults
|
|
291
|
+
to document.
|
|
292
|
+
</li>
|
|
293
|
+
<li>
|
|
294
|
+
<code class="rounded bg-gray-200 px-1">selectors</code> (string | string[]): CSS selector(s) to watch for
|
|
295
|
+
</li>
|
|
296
|
+
<li>
|
|
297
|
+
<code class="rounded bg-gray-200 px-1">fn</code> (function): Callback executed when matching elements are
|
|
298
|
+
found
|
|
299
|
+
</li>
|
|
300
|
+
</ul>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div>
|
|
304
|
+
<h3 class="mb-2 font-bold">Static Methods</h3>
|
|
305
|
+
<table class="w-full text-left text-sm">
|
|
306
|
+
<thead>
|
|
307
|
+
<tr class="border-b">
|
|
308
|
+
<th class="py-2 pr-4">Method</th>
|
|
309
|
+
<th class="py-2 pr-4">Signature</th>
|
|
310
|
+
<th class="py-2">Description</th>
|
|
311
|
+
</tr>
|
|
312
|
+
</thead>
|
|
313
|
+
<tbody>
|
|
314
|
+
<tr class="border-b">
|
|
315
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.ignore</code></td>
|
|
316
|
+
<td class="py-2 pr-4"><code class="text-xs">(...selectors: string[]) => void</code></td>
|
|
317
|
+
<td class="py-2">
|
|
318
|
+
Exclude elements inside matching containers from all callbacks. Calls are additive and shared across
|
|
319
|
+
bundles.
|
|
320
|
+
</td>
|
|
321
|
+
</tr>
|
|
322
|
+
<tr class="border-b">
|
|
323
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.pause</code></td>
|
|
324
|
+
<td class="py-2 pr-4"><code class="text-xs">() => void</code></td>
|
|
325
|
+
<td class="py-2">Pause mutation processing. Mutations that occur while paused trigger on resume.</td>
|
|
326
|
+
</tr>
|
|
327
|
+
<tr class="border-b">
|
|
328
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.resume</code></td>
|
|
329
|
+
<td class="py-2 pr-4"><code class="text-xs">() => void</code></td>
|
|
330
|
+
<td class="py-2">Resume mutation processing after a pause.</td>
|
|
331
|
+
</tr>
|
|
332
|
+
<tr>
|
|
333
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.triggerCallbacks</code></td>
|
|
334
|
+
<td class="py-2 pr-4"><code class="text-xs">(mutations?: MutationRecord[]) => void</code></td>
|
|
335
|
+
<td class="py-2">Manually trigger callback evaluation for all registered selectors.</td>
|
|
336
|
+
</tr>
|
|
337
|
+
</tbody>
|
|
338
|
+
</table>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div>
|
|
342
|
+
<h3 class="mb-2 font-bold">How It Works</h3>
|
|
343
|
+
<p class="text-gray-700">
|
|
344
|
+
onFind uses MutationObserver to efficiently watch for DOM changes. Each callback is executed only once per
|
|
345
|
+
element using data attributes to track already-processed elements. The observer watches for childList
|
|
346
|
+
mutations and specific attribute changes. Use <code class="rounded bg-gray-200 px-1">onFind.ignore()</code>
|
|
347
|
+
to exclude containers (e.g., rich-text editors) from triggering callbacks.
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
`,parameters:{docs:{description:{story:"Code examples and documentation for using the onFind utility."}}}};f.parameters={...f.parameters,docs:{...f.parameters?.docs,source:{originalSource:`{
|
|
352
|
+
render: args => {
|
|
353
|
+
const instanceId = \`onfind-\${Math.random().toString(36).substring(2, 9)}\`;
|
|
354
|
+
const uniqueSelector = \`.dynamic-item-\${instanceId}\`;
|
|
355
|
+
let itemCount = 0;
|
|
356
|
+
let foundCount = 0;
|
|
357
|
+
const updateCounts = () => {
|
|
358
|
+
const itemsEl = document.getElementById(\`\${instanceId}-items-count\`);
|
|
359
|
+
const foundEl = document.getElementById(\`\${instanceId}-found-count\`);
|
|
360
|
+
if (itemsEl) itemsEl.textContent = String(itemCount);
|
|
361
|
+
if (foundEl) foundEl.textContent = String(foundCount);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Set up onFind observer for dynamic items with unique selector
|
|
365
|
+
onFind(uniqueSelector, (element: HTMLElement) => {
|
|
366
|
+
foundCount++;
|
|
367
|
+
updateCounts();
|
|
368
|
+
element.classList.add('ring-2', 'ring-success-500');
|
|
369
|
+
});
|
|
370
|
+
const addItem = () => {
|
|
371
|
+
itemCount++;
|
|
372
|
+
const container = document.getElementById(\`\${instanceId}-container\`);
|
|
373
|
+
if (container) {
|
|
374
|
+
const item = document.createElement('div');
|
|
375
|
+
item.className = \`dynamic-item-\${instanceId} rounded border bg-white p-4\`;
|
|
376
|
+
item.textContent = \`Item \${itemCount}\`;
|
|
377
|
+
container.appendChild(item);
|
|
378
|
+
}
|
|
379
|
+
updateCounts();
|
|
380
|
+
};
|
|
381
|
+
const clearItems = () => {
|
|
382
|
+
itemCount = 0;
|
|
383
|
+
foundCount = 0;
|
|
384
|
+
const container = document.getElementById(\`\${instanceId}-container\`);
|
|
385
|
+
if (container) {
|
|
386
|
+
container.innerHTML = '';
|
|
387
|
+
}
|
|
388
|
+
updateCounts();
|
|
389
|
+
};
|
|
390
|
+
return html\`
|
|
391
|
+
<div class="space-y-4">
|
|
392
|
+
<div class="text-base">
|
|
393
|
+
<p class="mb-2">
|
|
394
|
+
Click "Add Item" to dynamically create elements. The onFind observer detects them and highlights them with a
|
|
395
|
+
green border.
|
|
396
|
+
</p>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div class="flex gap-2">
|
|
400
|
+
<button
|
|
401
|
+
data-testid="add-item"
|
|
402
|
+
@click=\${addItem}
|
|
403
|
+
class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
|
|
404
|
+
>
|
|
405
|
+
Add Item
|
|
406
|
+
</button>
|
|
407
|
+
<button
|
|
408
|
+
data-testid="clear-items"
|
|
409
|
+
@click=\${clearItems}
|
|
410
|
+
class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300"
|
|
411
|
+
>
|
|
412
|
+
Clear All
|
|
413
|
+
</button>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div id="\${instanceId}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
417
|
+
<div class="text-sm text-gray-500">Items will appear here...</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div class="flex justify-around">
|
|
421
|
+
<div>
|
|
422
|
+
<div class="text-xs text-gray-500">Items created</div>
|
|
423
|
+
<div data-testid="items-count" id="\${instanceId}-items-count" class="text-2xl font-bold text-gray-900">
|
|
424
|
+
0
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div>
|
|
428
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
429
|
+
<div data-testid="found-count" id="\${instanceId}-found-count" class="text-primary-600 text-2xl font-bold">
|
|
430
|
+
0
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
\`;
|
|
436
|
+
},
|
|
437
|
+
parameters: {
|
|
438
|
+
docs: {
|
|
439
|
+
description: {
|
|
440
|
+
story: \`Interactive example showing onFind detecting dynamically added elements. Elements are automatically highlighted when detected.\`
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
play: async ({
|
|
445
|
+
canvasElement,
|
|
446
|
+
step
|
|
447
|
+
}) => {
|
|
448
|
+
const canvas = within(canvasElement);
|
|
449
|
+
await step('Detects dynamically added element', async () => {
|
|
450
|
+
await userEvent.click(canvas.getByTestId('add-item'));
|
|
451
|
+
await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('1'));
|
|
452
|
+
});
|
|
453
|
+
await step('Detects multiple elements', async () => {
|
|
454
|
+
await userEvent.click(canvas.getByTestId('add-item'));
|
|
455
|
+
await userEvent.click(canvas.getByTestId('add-item'));
|
|
456
|
+
await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('3'));
|
|
457
|
+
});
|
|
458
|
+
await step('Clear resets counts', async () => {
|
|
459
|
+
await userEvent.click(canvas.getByTestId('clear-items'));
|
|
460
|
+
expect(canvas.getByTestId('items-count')).toHaveTextContent('0');
|
|
461
|
+
expect(canvas.getByTestId('found-count')).toHaveTextContent('0');
|
|
462
|
+
});
|
|
463
|
+
await step('Detection resumes after clear', async () => {
|
|
464
|
+
await userEvent.click(canvas.getByTestId('add-item'));
|
|
465
|
+
await waitFor(() => expect(canvas.getByTestId('found-count')).toHaveTextContent('1'));
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}`,...f.parameters?.docs?.source}}};B.parameters={...B.parameters,docs:{...B.parameters?.docs,source:{originalSource:`{
|
|
469
|
+
render: () => {
|
|
470
|
+
const instanceId = 'onfind-ignore-demo';
|
|
471
|
+
const itemSelector = '.ignore-demo-item';
|
|
472
|
+
const ignoredClass = 'ignore-demo-zone';
|
|
473
|
+
let foundCount = 0;
|
|
474
|
+
let itemCount = 0;
|
|
475
|
+
const updateCount = () => {
|
|
476
|
+
const el = document.getElementById(\`\${instanceId}-found-count\`);
|
|
477
|
+
if (el) el.textContent = String(foundCount);
|
|
478
|
+
};
|
|
479
|
+
onFind.ignore(\`.\${ignoredClass}\`);
|
|
480
|
+
onFind(itemSelector, (element: HTMLElement) => {
|
|
481
|
+
foundCount++;
|
|
482
|
+
updateCount();
|
|
483
|
+
element.classList.add('ring-2', 'ring-success-500');
|
|
484
|
+
});
|
|
485
|
+
const addOutside = () => {
|
|
486
|
+
itemCount++;
|
|
487
|
+
const container = document.getElementById(\`\${instanceId}-outside\`);
|
|
488
|
+
if (container) {
|
|
489
|
+
const item = document.createElement('div');
|
|
490
|
+
item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
|
|
491
|
+
item.textContent = \`Outside \${itemCount}\`;
|
|
492
|
+
container.appendChild(item);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const addInside = () => {
|
|
496
|
+
itemCount++;
|
|
497
|
+
const container = document.getElementById(\`\${instanceId}-inside\`);
|
|
498
|
+
if (container) {
|
|
499
|
+
const item = document.createElement('div');
|
|
500
|
+
item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
|
|
501
|
+
item.textContent = \`Inside \${itemCount}\`;
|
|
502
|
+
container.appendChild(item);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
return html\`
|
|
506
|
+
<div class="space-y-4">
|
|
507
|
+
<p class="text-base">
|
|
508
|
+
Elements added inside the <strong>ignored zone</strong> are skipped by onFind callbacks. Elements added
|
|
509
|
+
outside are detected normally.
|
|
510
|
+
</p>
|
|
511
|
+
|
|
512
|
+
<div class="flex gap-2">
|
|
513
|
+
<button
|
|
514
|
+
data-testid="add-outside"
|
|
515
|
+
@click=\${addOutside}
|
|
516
|
+
class="bg-primary-500 hover:bg-primary-600 rounded px-4 py-2 text-white"
|
|
517
|
+
>
|
|
518
|
+
Add Outside
|
|
519
|
+
</button>
|
|
520
|
+
<button data-testid="add-inside" @click=\${addInside} class="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300">
|
|
521
|
+
Add Inside Ignored
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<div class="grid grid-cols-2 gap-4">
|
|
526
|
+
<div>
|
|
527
|
+
<div class="mb-1 text-xs font-semibold text-gray-500">Normal zone</div>
|
|
528
|
+
<div id="\${instanceId}-outside" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
529
|
+
<div class="text-sm text-gray-400">Items detected here</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
<div>
|
|
533
|
+
<div class="mb-1 text-xs font-semibold text-gray-500">Ignored zone</div>
|
|
534
|
+
<div
|
|
535
|
+
id="\${instanceId}-inside"
|
|
536
|
+
class="\${ignoredClass} min-h-32 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
|
|
537
|
+
>
|
|
538
|
+
<div class="text-sm text-gray-400">Items ignored here</div>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div>
|
|
544
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
545
|
+
<div
|
|
546
|
+
data-testid="ignore-found-count"
|
|
547
|
+
id="\${instanceId}-found-count"
|
|
548
|
+
class="text-primary-600 text-2xl font-bold"
|
|
549
|
+
>
|
|
550
|
+
0
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
\`;
|
|
555
|
+
},
|
|
556
|
+
parameters: {
|
|
557
|
+
docs: {
|
|
558
|
+
description: {
|
|
559
|
+
story: \`Demonstrates \\\`onFind.ignore()\\\` — elements inside ignored containers are excluded from callback processing. Useful for skipping rich-text editors or other zones where DOM mutations should not trigger component initialization.\`
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
play: async ({
|
|
564
|
+
canvasElement,
|
|
565
|
+
step
|
|
566
|
+
}) => {
|
|
567
|
+
const canvas = within(canvasElement);
|
|
568
|
+
await step('Detects outside ignored zone', async () => {
|
|
569
|
+
await userEvent.click(canvas.getByTestId('add-outside'));
|
|
570
|
+
await waitFor(() => expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('1'));
|
|
571
|
+
});
|
|
572
|
+
await step('Skips inside ignored zone', async () => {
|
|
573
|
+
await userEvent.click(canvas.getByTestId('add-inside'));
|
|
574
|
+
// Wait a frame to ensure mutation observer has processed
|
|
575
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
576
|
+
expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('1');
|
|
577
|
+
});
|
|
578
|
+
await step('Continues detecting outside', async () => {
|
|
579
|
+
await userEvent.click(canvas.getByTestId('add-outside'));
|
|
580
|
+
await waitFor(() => expect(canvas.getByTestId('ignore-found-count')).toHaveTextContent('2'));
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}`,...B.parameters?.docs?.source}}};w.parameters={...w.parameters,docs:{...w.parameters?.docs,source:{originalSource:`{
|
|
584
|
+
render: () => {
|
|
585
|
+
const instanceId = 'onfind-pause-demo';
|
|
586
|
+
const itemSelector = '.pause-demo-item';
|
|
587
|
+
let foundCount = 0;
|
|
588
|
+
let itemCount = 0;
|
|
589
|
+
const updateCount = () => {
|
|
590
|
+
const el = document.getElementById(\`\${instanceId}-found-count\`);
|
|
591
|
+
if (el) el.textContent = String(foundCount);
|
|
592
|
+
};
|
|
593
|
+
onFind(itemSelector, (element: HTMLElement) => {
|
|
594
|
+
foundCount++;
|
|
595
|
+
updateCount();
|
|
596
|
+
element.classList.add('ring-2', 'ring-success-500');
|
|
597
|
+
});
|
|
598
|
+
const addItem = () => {
|
|
599
|
+
itemCount++;
|
|
600
|
+
const container = document.getElementById(\`\${instanceId}-container\`);
|
|
601
|
+
if (container) {
|
|
602
|
+
const item = document.createElement('div');
|
|
603
|
+
item.className = \`\${itemSelector.slice(1)} rounded border bg-white p-4\`;
|
|
604
|
+
item.textContent = \`Item \${itemCount}\`;
|
|
605
|
+
container.appendChild(item);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
let paused = false;
|
|
609
|
+
const togglePause = () => {
|
|
610
|
+
paused = !paused;
|
|
611
|
+
if (paused) {
|
|
612
|
+
onFind.pause();
|
|
613
|
+
} else {
|
|
614
|
+
onFind.resume();
|
|
615
|
+
}
|
|
616
|
+
const btn = document.querySelector('[data-testid="toggle-pause"]') as HTMLButtonElement;
|
|
617
|
+
if (btn) {
|
|
618
|
+
btn.textContent = paused ? 'Resume' : 'Pause';
|
|
619
|
+
btn.className = paused ? 'btu-button btu-button-success btu-button-sm' : 'btu-button btu-button-warning btu-button-sm';
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
return html\`
|
|
623
|
+
<div class="space-y-4">
|
|
624
|
+
<p class="text-base">
|
|
625
|
+
Use <code>onFind.pause()</code> to temporarily stop detection and <code>onFind.resume()</code> to restart it.
|
|
626
|
+
Elements added while paused are detected on resume.
|
|
627
|
+
</p>
|
|
628
|
+
|
|
629
|
+
<div class="flex gap-2">
|
|
630
|
+
<button data-testid="pause-add" @click=\${addItem} class="btu-button btu-button-primary btu-button-sm">
|
|
631
|
+
Add Item
|
|
632
|
+
</button>
|
|
633
|
+
<button data-testid="toggle-pause" @click=\${togglePause} class="btu-button btu-button-warning btu-button-sm">
|
|
634
|
+
Pause
|
|
635
|
+
</button>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<div id="\${instanceId}-container" class="min-h-32 space-y-2 rounded border-2 border-gray-300 bg-gray-50 p-4">
|
|
639
|
+
<div class="text-sm text-gray-500">Items will appear here...</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div>
|
|
643
|
+
<div class="text-xs text-gray-500">Items detected by onFind</div>
|
|
644
|
+
<div
|
|
645
|
+
data-testid="pause-found-count"
|
|
646
|
+
id="\${instanceId}-found-count"
|
|
647
|
+
class="text-primary-600 text-2xl font-bold"
|
|
648
|
+
>
|
|
649
|
+
0
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
\`;
|
|
654
|
+
},
|
|
655
|
+
parameters: {
|
|
656
|
+
docs: {
|
|
657
|
+
description: {
|
|
658
|
+
story: \`Demonstrates \\\`onFind.pause()\\\` and \\\`onFind.resume()\\\`. While paused, DOM mutations are queued and processed when resumed.\`
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
play: async ({
|
|
663
|
+
canvasElement,
|
|
664
|
+
step
|
|
665
|
+
}) => {
|
|
666
|
+
const canvas = within(canvasElement);
|
|
667
|
+
await step('Normal detection before pause', async () => {
|
|
668
|
+
await userEvent.click(canvas.getByTestId('pause-add'));
|
|
669
|
+
await waitFor(() => expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('1'));
|
|
670
|
+
});
|
|
671
|
+
await step('Pause stops detection', async () => {
|
|
672
|
+
await userEvent.click(canvas.getByTestId('toggle-pause'));
|
|
673
|
+
await userEvent.click(canvas.getByTestId('pause-add'));
|
|
674
|
+
// Wait enough time for normal detection to fire if it were going to
|
|
675
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
676
|
+
expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('1');
|
|
677
|
+
});
|
|
678
|
+
await step('Resume triggers pending detection', async () => {
|
|
679
|
+
await userEvent.click(canvas.getByTestId('toggle-pause'));
|
|
680
|
+
// resume() does setTimeout(triggerCallbacks, 100) then rAF — needs generous timeout
|
|
681
|
+
await waitFor(() => expect(canvas.getByTestId('pause-found-count')).toHaveTextContent('2'), {
|
|
682
|
+
timeout: 2000
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}`,...w.parameters?.docs?.source}}};I.parameters={...I.parameters,docs:{...I.parameters?.docs,source:{originalSource:`{
|
|
687
|
+
render: () => {
|
|
688
|
+
const onFindA = onFind;
|
|
689
|
+
const selectorA = '.singleton-item-a';
|
|
690
|
+
const selectorB = '.singleton-item-b';
|
|
691
|
+
let countA = 0;
|
|
692
|
+
let countB = 0;
|
|
693
|
+
const updateCounts = () => {
|
|
694
|
+
const elA = document.getElementById('singleton-count-a');
|
|
695
|
+
const elB = document.getElementById('singleton-count-b');
|
|
696
|
+
if (elA) elA.textContent = String(countA);
|
|
697
|
+
if (elB) elB.textContent = String(countB);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// Bundle A sets up the ignore list — Bundle B should respect it
|
|
701
|
+
onFindA.ignore('.singleton-ignored-zone');
|
|
702
|
+
|
|
703
|
+
// Bundle A registers selectorA
|
|
704
|
+
onFindA(selectorA, (element: HTMLElement) => {
|
|
705
|
+
countA++;
|
|
706
|
+
updateCounts();
|
|
707
|
+
element.classList.add('ring-2', 'ring-blue-500');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Bundle B (separate module scope, own _state cache) registers selectorB
|
|
711
|
+
onFindB(selectorB, (element: HTMLElement) => {
|
|
712
|
+
countB++;
|
|
713
|
+
updateCounts();
|
|
714
|
+
element.classList.add('ring-2', 'ring-purple-500');
|
|
715
|
+
});
|
|
716
|
+
const addItem = (selector: string, label: string, zoneId: string) => {
|
|
717
|
+
const container = document.getElementById(zoneId);
|
|
718
|
+
if (container) {
|
|
719
|
+
const item = document.createElement('div');
|
|
720
|
+
item.className = \`\${selector.slice(1)} rounded border bg-white p-4\`;
|
|
721
|
+
item.textContent = label;
|
|
722
|
+
container.appendChild(item);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
let paused = false;
|
|
726
|
+
|
|
727
|
+
// Pause/resume via Bundle A — should also affect Bundle B's callbacks
|
|
728
|
+
const togglePause = () => {
|
|
729
|
+
paused = !paused;
|
|
730
|
+
if (paused) {
|
|
731
|
+
onFindA.pause();
|
|
732
|
+
} else {
|
|
733
|
+
onFindA.resume();
|
|
734
|
+
}
|
|
735
|
+
const btn = document.querySelector('[data-testid="singleton-toggle-pause"]') as HTMLButtonElement;
|
|
736
|
+
if (btn) {
|
|
737
|
+
btn.textContent = paused ? 'Resume' : 'Pause';
|
|
738
|
+
btn.className = paused ? 'btu-button btu-button-success btu-button-sm' : 'btu-button btu-button-warning btu-button-sm';
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const addIgnored = () => {
|
|
742
|
+
addItem(selectorA, 'Ignored A', 'singleton-ignored');
|
|
743
|
+
addItem(selectorB, 'Ignored B', 'singleton-ignored');
|
|
744
|
+
};
|
|
745
|
+
return html\`
|
|
746
|
+
<div class="space-y-4">
|
|
747
|
+
<p class="text-base">
|
|
748
|
+
Two separate ES module instances of <code>onFind</code> (each with its own <code>_state</code> cache) share
|
|
749
|
+
one observer, ignore list, and pause/resume state via <code>globalThis[Symbol.for()]</code>.
|
|
750
|
+
</p>
|
|
751
|
+
|
|
752
|
+
<div class="flex items-center gap-4">
|
|
753
|
+
<button
|
|
754
|
+
data-testid="singleton-toggle-pause"
|
|
755
|
+
@click=\${togglePause}
|
|
756
|
+
class="btu-button btu-button-warning btu-button-sm"
|
|
757
|
+
>
|
|
758
|
+
Pause
|
|
759
|
+
</button>
|
|
760
|
+
<div class="flex items-center gap-1 text-sm text-blue-600">
|
|
761
|
+
Bundle A:
|
|
762
|
+
<span data-testid="singleton-count-a" id="singleton-count-a" class="font-bold">0</span>
|
|
763
|
+
items found
|
|
764
|
+
</div>
|
|
765
|
+
<div class="flex items-center gap-1 text-sm text-purple-600">
|
|
766
|
+
Bundle B:
|
|
767
|
+
<span data-testid="singleton-count-b" id="singleton-count-b" class="font-bold">0</span>
|
|
768
|
+
items found
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="grid grid-cols-3 gap-4">
|
|
773
|
+
<div>
|
|
774
|
+
<div class="mb-2 text-xs font-semibold text-blue-600">Bundle A zone</div>
|
|
775
|
+
<div class="mb-2 flex gap-1">
|
|
776
|
+
<button
|
|
777
|
+
data-testid="singleton-add-a"
|
|
778
|
+
@click=\${() => addItem(selectorA, 'Bundle A item', 'singleton-zone-a')}
|
|
779
|
+
class="btu-button btu-button-primary btu-button-sm"
|
|
780
|
+
>
|
|
781
|
+
Add A
|
|
782
|
+
</button>
|
|
783
|
+
<button
|
|
784
|
+
data-testid="singleton-zone-a-add-b"
|
|
785
|
+
@click=\${() => addItem(selectorB, 'Bundle B item', 'singleton-zone-a')}
|
|
786
|
+
class="btu-button btu-button-purple btu-button-sm"
|
|
787
|
+
>
|
|
788
|
+
Add B
|
|
789
|
+
</button>
|
|
790
|
+
</div>
|
|
791
|
+
<div id="singleton-zone-a" class="min-h-24 space-y-2 rounded border-2 border-blue-300 bg-blue-50 p-4">
|
|
792
|
+
<div class="text-sm text-gray-400">A items here</div>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
<div>
|
|
796
|
+
<div class="mb-2 text-xs font-semibold text-purple-600">Bundle B zone</div>
|
|
797
|
+
<div class="mb-2 flex gap-1">
|
|
798
|
+
<button
|
|
799
|
+
data-testid="singleton-zone-b-add-a"
|
|
800
|
+
@click=\${() => addItem(selectorA, 'Bundle A item', 'singleton-zone-b')}
|
|
801
|
+
class="btu-button btu-button-primary btu-button-sm"
|
|
802
|
+
>
|
|
803
|
+
Add A
|
|
804
|
+
</button>
|
|
805
|
+
<button
|
|
806
|
+
data-testid="singleton-add-b"
|
|
807
|
+
@click=\${() => addItem(selectorB, 'Bundle B item', 'singleton-zone-b')}
|
|
808
|
+
class="btu-button btu-button-purple btu-button-sm"
|
|
809
|
+
>
|
|
810
|
+
Add B
|
|
811
|
+
</button>
|
|
812
|
+
</div>
|
|
813
|
+
<div id="singleton-zone-b" class="min-h-24 space-y-2 rounded border-2 border-purple-300 bg-purple-50 p-4">
|
|
814
|
+
<div class="text-sm text-gray-400">B items here</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
<div>
|
|
818
|
+
<div class="mb-2 text-xs font-semibold text-red-600">Ignored zone</div>
|
|
819
|
+
<div class="mb-2 flex gap-1">
|
|
820
|
+
<button
|
|
821
|
+
data-testid="singleton-add-ignored"
|
|
822
|
+
@click=\${addIgnored}
|
|
823
|
+
class="btu-button btu-button-gray btu-button-sm"
|
|
824
|
+
>
|
|
825
|
+
Add A & B
|
|
826
|
+
</button>
|
|
827
|
+
</div>
|
|
828
|
+
<div
|
|
829
|
+
id="singleton-ignored"
|
|
830
|
+
class="singleton-ignored-zone min-h-24 space-y-2 rounded border-2 border-red-300 bg-red-50 p-4"
|
|
831
|
+
>
|
|
832
|
+
<div class="text-sm text-gray-400">Items ignored here</div>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
\`;
|
|
838
|
+
},
|
|
839
|
+
parameters: {
|
|
840
|
+
docs: {
|
|
841
|
+
description: {
|
|
842
|
+
story: \`Two independent module instances of \\\`onFind\\\` (simulating separate bundles) share a single MutationObserver, ignore list, and pause/resume state through the \\\`globalThis[Symbol.for()]\\\` singleton pattern.\`
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
play: async ({
|
|
847
|
+
canvasElement,
|
|
848
|
+
step
|
|
849
|
+
}) => {
|
|
850
|
+
const canvas = within(canvasElement);
|
|
851
|
+
await step('Shared observer: Bundle A detects A items, Bundle B detects B items', async () => {
|
|
852
|
+
await userEvent.click(canvas.getByTestId('singleton-add-a'));
|
|
853
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('1'));
|
|
854
|
+
await userEvent.click(canvas.getByTestId('singleton-add-b'));
|
|
855
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('1'));
|
|
856
|
+
});
|
|
857
|
+
await step('Cross-zone detection fires correct callback', async () => {
|
|
858
|
+
// Add B-selector item to zone A — should increment B count, not A
|
|
859
|
+
await userEvent.click(canvas.getByTestId('singleton-zone-a-add-b'));
|
|
860
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2'));
|
|
861
|
+
expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('1');
|
|
862
|
+
|
|
863
|
+
// Add A-selector item to zone B — should increment A count, not B
|
|
864
|
+
await userEvent.click(canvas.getByTestId('singleton-zone-b-add-a'));
|
|
865
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('2'));
|
|
866
|
+
expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2');
|
|
867
|
+
});
|
|
868
|
+
await step('Shared ignore list: Bundle A ignore() blocks Bundle B callbacks', async () => {
|
|
869
|
+
const countABefore = canvas.getByTestId('singleton-count-a').textContent;
|
|
870
|
+
const countBBefore = canvas.getByTestId('singleton-count-b').textContent;
|
|
871
|
+
await userEvent.click(canvas.getByTestId('singleton-add-ignored'));
|
|
872
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
873
|
+
expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent(countABefore!);
|
|
874
|
+
expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent(countBBefore!);
|
|
875
|
+
});
|
|
876
|
+
await step('Ignore lists are merged and deduped across bundles', async () => {
|
|
877
|
+
const state = (globalThis as any)[Symbol.for('brightspot.onFind')];
|
|
878
|
+
const before = state.blacklist.length;
|
|
879
|
+
|
|
880
|
+
// Bundle B ignores the same selector Bundle A already added in render()
|
|
881
|
+
onFindB.ignore('.singleton-ignored-zone');
|
|
882
|
+
expect(state.blacklist.length).toBe(before); // no duplicate
|
|
883
|
+
|
|
884
|
+
// Bundle B adds a new selector — it merges into the shared list
|
|
885
|
+
onFindB.ignore('.bundle-b-ignored');
|
|
886
|
+
expect(state.blacklist).toContain('.singleton-ignored-zone');
|
|
887
|
+
expect(state.blacklist).toContain('.bundle-b-ignored');
|
|
888
|
+
expect(state.blacklist.length).toBe(before + 1);
|
|
889
|
+
});
|
|
890
|
+
await step('Shared pause/resume: Bundle A pause() stops Bundle B detection', async () => {
|
|
891
|
+
await userEvent.click(canvas.getByTestId('singleton-toggle-pause'));
|
|
892
|
+
await userEvent.click(canvas.getByTestId('singleton-add-a'));
|
|
893
|
+
await userEvent.click(canvas.getByTestId('singleton-add-b'));
|
|
894
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
895
|
+
|
|
896
|
+
// Still at previous counts while paused
|
|
897
|
+
expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('2');
|
|
898
|
+
expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('2');
|
|
899
|
+
await userEvent.click(canvas.getByTestId('singleton-toggle-pause'));
|
|
900
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-a')).toHaveTextContent('3'), {
|
|
901
|
+
timeout: 2000
|
|
902
|
+
});
|
|
903
|
+
await waitFor(() => expect(canvas.getByTestId('singleton-count-b')).toHaveTextContent('3'), {
|
|
904
|
+
timeout: 2000
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
await step('No index collision on data-ofc attributes', async () => {
|
|
908
|
+
const allDetected = canvasElement.querySelectorAll('.singleton-item-a, .singleton-item-b');
|
|
909
|
+
const indices = new Set<string>();
|
|
910
|
+
allDetected.forEach(el => {
|
|
911
|
+
el.getAttributeNames().filter(name => name.startsWith('data-ofc')).forEach(name => indices.add(name));
|
|
912
|
+
});
|
|
913
|
+
expect(indices.size).toBeGreaterThanOrEqual(2);
|
|
914
|
+
});
|
|
915
|
+
await step('Singleton state: both bundles share the same globalThis object', async () => {
|
|
916
|
+
const state = (globalThis as any)[Symbol.for('brightspot.onFind')];
|
|
917
|
+
expect(state).toBeDefined();
|
|
918
|
+
expect(state.callbacks).toBeDefined();
|
|
919
|
+
expect(Array.isArray(state.callbacks)).toBe(true);
|
|
920
|
+
expect(state.initialized).toBe(true);
|
|
921
|
+
|
|
922
|
+
// Function refs are different (proving separate module scopes) but state is shared
|
|
923
|
+
expect(onFind.pause).not.toBe(onFindB.pause);
|
|
924
|
+
expect(onFind.resume).not.toBe(onFindB.resume);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}`,...I.parameters?.docs?.source}}};T.parameters={...T.parameters,docs:{...T.parameters?.docs,source:{originalSource:`{
|
|
928
|
+
render: () => html\`
|
|
929
|
+
<div class="space-y-4 text-sm">
|
|
930
|
+
<div>
|
|
931
|
+
<h3 class="mb-2 font-bold">Basic Usage</h3>
|
|
932
|
+
<pre
|
|
933
|
+
class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
|
|
934
|
+
><code>import onFind from '@brightspot/ui/util/onFind.js'
|
|
935
|
+
|
|
936
|
+
// Watch for elements globally
|
|
937
|
+
onFind('.my-component', (element) => {
|
|
938
|
+
console.log('Found element:', element)
|
|
939
|
+
// Initialize component
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
// Watch within a specific root element
|
|
943
|
+
const container = document.querySelector('#container')
|
|
944
|
+
onFind(container, '.my-widget', (element) => {
|
|
945
|
+
// Initialize widget
|
|
946
|
+
})</code></pre>
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
<div>
|
|
950
|
+
<h3 class="mb-2 font-bold">Multiple Selectors</h3>
|
|
951
|
+
<pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Watch for multiple selectors at once
|
|
952
|
+
onFind(['.button', '.input', '.select'], (element) => {
|
|
953
|
+
if (element.matches('.button')) {
|
|
954
|
+
// Handle button
|
|
955
|
+
} else if (element.matches('.input')) {
|
|
956
|
+
// Handle input
|
|
957
|
+
}
|
|
958
|
+
})</code></pre>
|
|
959
|
+
</div>
|
|
960
|
+
|
|
961
|
+
<div>
|
|
962
|
+
<h3 class="mb-2 font-bold">Pause and Resume</h3>
|
|
963
|
+
<pre class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"><code>// Pause observations temporarily
|
|
964
|
+
onFind.pause()
|
|
965
|
+
|
|
966
|
+
// Make bulk DOM changes without triggering callbacks
|
|
967
|
+
// ...
|
|
968
|
+
|
|
969
|
+
// Resume observations
|
|
970
|
+
onFind.resume()</code></pre>
|
|
971
|
+
</div>
|
|
972
|
+
|
|
973
|
+
<div>
|
|
974
|
+
<h3 class="mb-2 font-bold">Ignore Selectors</h3>
|
|
975
|
+
<pre
|
|
976
|
+
class="overflow-x-auto rounded bg-gray-900 p-4 text-gray-100"
|
|
977
|
+
><code>// Exclude elements inside matching containers from all callbacks.
|
|
978
|
+
// Calls are additive and shared across bundles.
|
|
979
|
+
onFind.ignore('.ProseMirror', '.RichTextEditor')
|
|
980
|
+
|
|
981
|
+
// Elements inside ignored containers will not trigger callbacks,
|
|
982
|
+
// even if they match a registered selector.</code></pre>
|
|
983
|
+
</div>
|
|
984
|
+
|
|
985
|
+
<div>
|
|
986
|
+
<h3 class="mb-2 font-bold">Common Use Cases</h3>
|
|
987
|
+
<ul class="list-inside list-disc space-y-1 text-gray-700">
|
|
988
|
+
<li>Initialize components when they appear in the DOM</li>
|
|
989
|
+
<li>Attach event listeners to dynamically created elements</li>
|
|
990
|
+
<li>Enhance server-rendered content progressively</li>
|
|
991
|
+
<li>Detect and respond to attribute changes</li>
|
|
992
|
+
<li>Apply transformations to newly added content</li>
|
|
993
|
+
</ul>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div>
|
|
997
|
+
<h3 class="mb-2 font-bold">Parameters</h3>
|
|
998
|
+
<ul class="space-y-2">
|
|
999
|
+
<li>
|
|
1000
|
+
<code class="rounded bg-gray-200 px-1">root</code> (ParentNode, optional): Root element to observe. Defaults
|
|
1001
|
+
to document.
|
|
1002
|
+
</li>
|
|
1003
|
+
<li>
|
|
1004
|
+
<code class="rounded bg-gray-200 px-1">selectors</code> (string | string[]): CSS selector(s) to watch for
|
|
1005
|
+
</li>
|
|
1006
|
+
<li>
|
|
1007
|
+
<code class="rounded bg-gray-200 px-1">fn</code> (function): Callback executed when matching elements are
|
|
1008
|
+
found
|
|
1009
|
+
</li>
|
|
1010
|
+
</ul>
|
|
1011
|
+
</div>
|
|
1012
|
+
|
|
1013
|
+
<div>
|
|
1014
|
+
<h3 class="mb-2 font-bold">Static Methods</h3>
|
|
1015
|
+
<table class="w-full text-left text-sm">
|
|
1016
|
+
<thead>
|
|
1017
|
+
<tr class="border-b">
|
|
1018
|
+
<th class="py-2 pr-4">Method</th>
|
|
1019
|
+
<th class="py-2 pr-4">Signature</th>
|
|
1020
|
+
<th class="py-2">Description</th>
|
|
1021
|
+
</tr>
|
|
1022
|
+
</thead>
|
|
1023
|
+
<tbody>
|
|
1024
|
+
<tr class="border-b">
|
|
1025
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.ignore</code></td>
|
|
1026
|
+
<td class="py-2 pr-4"><code class="text-xs">(...selectors: string[]) => void</code></td>
|
|
1027
|
+
<td class="py-2">
|
|
1028
|
+
Exclude elements inside matching containers from all callbacks. Calls are additive and shared across
|
|
1029
|
+
bundles.
|
|
1030
|
+
</td>
|
|
1031
|
+
</tr>
|
|
1032
|
+
<tr class="border-b">
|
|
1033
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.pause</code></td>
|
|
1034
|
+
<td class="py-2 pr-4"><code class="text-xs">() => void</code></td>
|
|
1035
|
+
<td class="py-2">Pause mutation processing. Mutations that occur while paused trigger on resume.</td>
|
|
1036
|
+
</tr>
|
|
1037
|
+
<tr class="border-b">
|
|
1038
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.resume</code></td>
|
|
1039
|
+
<td class="py-2 pr-4"><code class="text-xs">() => void</code></td>
|
|
1040
|
+
<td class="py-2">Resume mutation processing after a pause.</td>
|
|
1041
|
+
</tr>
|
|
1042
|
+
<tr>
|
|
1043
|
+
<td class="py-2 pr-4"><code class="rounded bg-gray-200 px-1">onFind.triggerCallbacks</code></td>
|
|
1044
|
+
<td class="py-2 pr-4"><code class="text-xs">(mutations?: MutationRecord[]) => void</code></td>
|
|
1045
|
+
<td class="py-2">Manually trigger callback evaluation for all registered selectors.</td>
|
|
1046
|
+
</tr>
|
|
1047
|
+
</tbody>
|
|
1048
|
+
</table>
|
|
1049
|
+
</div>
|
|
1050
|
+
|
|
1051
|
+
<div>
|
|
1052
|
+
<h3 class="mb-2 font-bold">How It Works</h3>
|
|
1053
|
+
<p class="text-gray-700">
|
|
1054
|
+
onFind uses MutationObserver to efficiently watch for DOM changes. Each callback is executed only once per
|
|
1055
|
+
element using data attributes to track already-processed elements. The observer watches for childList
|
|
1056
|
+
mutations and specific attribute changes. Use <code class="rounded bg-gray-200 px-1">onFind.ignore()</code>
|
|
1057
|
+
to exclude containers (e.g., rich-text editors) from triggering callbacks.
|
|
1058
|
+
</p>
|
|
1059
|
+
</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
\`,
|
|
1062
|
+
parameters: {
|
|
1063
|
+
docs: {
|
|
1064
|
+
description: {
|
|
1065
|
+
story: \`Code examples and documentation for using the onFind utility.\`
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}`,...T.parameters?.docs?.source}}};const W=["Interactive","Ignore","PauseResume","Singleton","UsageExample"];export{B as Ignore,f as Interactive,w as PauseResume,I as Singleton,T as UsageExample,W as __namedExportsOrder,U as default};
|