-
-
Notifications
You must be signed in to change notification settings - Fork 183
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f09ff85
commit eb71907
Showing
7 changed files
with
193 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import * as accordion from "@zag-js/accordion" | ||
import { normalizeProps } from "./normalize-props" | ||
import { spreadProps } from "./spread-props" | ||
|
||
export class Accordion { | ||
rootEl: HTMLElement | ||
service: ReturnType<typeof accordion.machine> | ||
api: accordion.Api<any> | ||
|
||
constructor(root: string, context: accordion.Context) { | ||
const rootEl = document.querySelector<HTMLElement>(root) | ||
|
||
if (!rootEl) throw new Error("Root element not found") | ||
this.rootEl = rootEl | ||
|
||
this.service = accordion.machine(context) | ||
this.api = accordion.connect(this.service.state, this.service.send, normalizeProps) | ||
} | ||
|
||
private disposable = new Map<HTMLElement, VoidFunction>() | ||
|
||
init = () => { | ||
const { service } = this | ||
this.service.subscribe(() => { | ||
this.api = accordion.connect(service.state, service.send, normalizeProps) | ||
this.render() | ||
}) | ||
|
||
this.service.start() | ||
} | ||
|
||
destroy = () => { | ||
this.service.stop() | ||
} | ||
|
||
private get items() { | ||
return Array.from(this.rootEl!.querySelectorAll<HTMLElement>(".accordion-item")) | ||
} | ||
|
||
private renderRoot = () => { | ||
const rootEl = this.rootEl | ||
this.disposable.set(rootEl, spreadProps(this.rootEl, this.api.rootProps)) | ||
} | ||
|
||
private renderItem = (itemEl: HTMLElement) => { | ||
const value = itemEl.dataset.value | ||
if (!value) throw new Error("Expected value to be defined") | ||
|
||
const itemTriggerEl = itemEl.querySelector<HTMLButtonElement>(".accordion-trigger") | ||
const itemContentEl = itemEl.querySelector<HTMLElement>(".accordion-content") | ||
|
||
if (!itemTriggerEl) throw new Error("Expected triggerEl to be defined") | ||
if (!itemContentEl) throw new Error("Expected contentEl to be defined") | ||
|
||
const cleanup = this.disposable.get(itemEl) | ||
cleanup?.() | ||
|
||
const cleanups = [ | ||
spreadProps(itemEl, this.api.getItemProps({ value })), | ||
spreadProps(itemTriggerEl, this.api.getItemTriggerProps({ value })), | ||
spreadProps(itemContentEl, this.api.getItemContentProps({ value })), | ||
] | ||
|
||
this.disposable.set(itemEl, () => { | ||
cleanups.forEach((fn) => fn()) | ||
}) | ||
} | ||
|
||
render = () => { | ||
this.renderRoot() | ||
this.items.forEach((itemEl) => { | ||
this.renderItem(itemEl) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,9 @@ | ||
import * as checkbox from "@zag-js/checkbox" | ||
import { subscribe } from "@zag-js/core" | ||
import { createNormalizer } from "@zag-js/types" | ||
import Alpine from "alpinejs" | ||
import "../../../shared/src/style.css" | ||
import { Accordion } from "./accordion" | ||
import { nanoid } from "nanoid" | ||
|
||
const propMap: any = { | ||
htmlFor: "for", | ||
className: "class", | ||
onDoubleClick: "onDblclick", | ||
onChange: "onInput", | ||
onFocus: "onFocusin", | ||
onBlur: "onFocusout", | ||
defaultValue: "value", | ||
defaultChecked: "checked", | ||
} | ||
|
||
const toStyleString = (style: any) => { | ||
let string = "" | ||
for (let key in style) { | ||
const value = style[key] | ||
if (value === null || value === undefined) continue | ||
if (!key.startsWith("--")) key = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) | ||
string += `${key}:${value};` | ||
} | ||
return string | ||
} | ||
|
||
// all event handlers should use the format @click | ||
const normalizeProps = createNormalizer((props: any) => { | ||
return Object.entries(props).reduce<any>((acc, [key, value]) => { | ||
if (value === undefined) return acc | ||
|
||
if (key in propMap) { | ||
key = propMap[key] | ||
} | ||
|
||
if (key === "style" && typeof value === "object") { | ||
acc.style = toStyleString(value) | ||
return acc | ||
} | ||
|
||
if (key.startsWith("on")) { | ||
const _key = key.replace(/^on/, "@").toLowerCase() | ||
acc[_key] = value | ||
} else { | ||
acc[`:${key.toLowerCase()}`] = () => value | ||
} | ||
|
||
return acc | ||
}, {}) | ||
}) | ||
|
||
document.addEventListener("alpine:init", () => { | ||
Alpine.magic("checkbox", (el, { Alpine, effect }) => (context: any) => { | ||
const service = checkbox.machine(context) | ||
|
||
const state = Alpine.reactive({ value: service.getState() }) | ||
|
||
service.start() | ||
|
||
effect(() => { | ||
service.setContext(context) | ||
|
||
const unsubscribe = subscribe(service.state, () => { | ||
state.value = service.getState() | ||
}) | ||
|
||
return () => { | ||
unsubscribe() | ||
service.stop() | ||
} | ||
}) | ||
|
||
Alpine.bind(el, { | ||
"x-data"() { | ||
return { | ||
get api() { | ||
return checkbox.connect(state.value, service.send, normalizeProps) | ||
}, | ||
} | ||
}, | ||
}) | ||
}) | ||
const accordion = new Accordion(".accordion", { | ||
id: nanoid(), | ||
multiple: true, | ||
}) | ||
|
||
Alpine.start() | ||
accordion.init() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { createNormalizer } from "@zag-js/types" | ||
|
||
export interface AttrMap { | ||
[key: string]: string | ||
} | ||
|
||
export const propMap: AttrMap = { | ||
onFocus: "onFocusin", | ||
onBlur: "onFocusout", | ||
onChange: "onInput", | ||
onDoubleClick: "onDblclick", | ||
htmlFor: "for", | ||
className: "class", | ||
defaultValue: "value", | ||
defaultChecked: "checked", | ||
} | ||
|
||
const toStyleString = (style: any) => { | ||
let string = "" | ||
for (let key in style) { | ||
const value = style[key] | ||
if (value === null || value === undefined) continue | ||
if (!key.startsWith("--")) key = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) | ||
string += `${key}:${value};` | ||
} | ||
return string | ||
} | ||
|
||
export const normalizeProps = createNormalizer((props: any) => { | ||
return Object.entries(props).reduce<any>((acc, [key, value]) => { | ||
if (value === undefined) return acc | ||
|
||
if (key in propMap) { | ||
key = propMap[key] | ||
} | ||
|
||
if (key === "style" && typeof value === "object") { | ||
acc.style = toStyleString(value) | ||
return acc | ||
} | ||
|
||
acc[key.toLowerCase()] = value | ||
|
||
return acc | ||
}, {}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
export interface Attrs { | ||
[key: string]: any // Change 'any' to the specific type you want to allow for attributes | ||
} | ||
|
||
export function spreadProps(node: HTMLElement, attrs: Attrs): () => void { | ||
const attrKeys = Object.keys(attrs) | ||
|
||
const addEvt = (e: string, f: EventListener) => { | ||
node.addEventListener(e.toLowerCase(), f) | ||
} | ||
|
||
const remEvt = (e: string, f: EventListener) => { | ||
node.removeEventListener(e.toLowerCase(), f) | ||
} | ||
|
||
const onEvents = (attr: string) => attr.startsWith("on") | ||
const others = (attr: string) => !attr.startsWith("on") | ||
|
||
const setup = (attr: string) => addEvt(attr.substring(2), attrs[attr]) | ||
const teardown = (attr: string) => remEvt(attr.substring(2), attrs[attr]) | ||
|
||
const apply = (attrName: string) => { | ||
let value = attrs[attrName] | ||
|
||
if (typeof value === "boolean") { | ||
value = value || undefined | ||
} | ||
|
||
if (value != null) { | ||
if (["value", "checked", "htmlFor"].includes(attrName)) { | ||
;(node as any)[attrName] = value // Using 'any' here because TypeScript can't narrow the type based on the array check | ||
} else { | ||
node.setAttribute(attrName.toLowerCase(), value) | ||
} | ||
return | ||
} | ||
|
||
node.removeAttribute(attrName.toLowerCase()) | ||
} | ||
|
||
attrKeys.filter(onEvents).forEach(setup) | ||
attrKeys.filter(others).forEach(apply) | ||
|
||
return function cleanup() { | ||
attrKeys.filter(onEvents).forEach(teardown) | ||
} | ||
} |
Oops, something went wrong.