2023-01-23 09:07:55 -05:00
|
|
|
import { html } from "htm";
|
|
|
|
import * as React from "preact";
|
|
|
|
|
2023-01-24 17:28:17 -05:00
|
|
|
/** @typedef {(props:{children:preact.VNode, class:string|null})=>preact.VNode} BasicElement */
|
2023-01-24 10:13:43 -05:00
|
|
|
/** @typedef {{Open:boolean, Done:boolean}} Stage*/
|
2023-01-23 09:07:55 -05:00
|
|
|
/** @typedef {[set:Stage, get:React.StateUpdater<Stage>]} Binding */
|
|
|
|
|
|
|
|
const BranchContext = React.createContext(
|
2023-01-24 10:13:43 -05:00
|
|
|
/** @type Binding */ ([{ Open: false, Done: true }, (_n) => {}]),
|
2023-01-23 09:07:55 -05:00
|
|
|
);
|
|
|
|
|
2023-01-24 17:28:17 -05:00
|
|
|
export { BranchContext as Context };
|
|
|
|
|
2023-01-24 10:13:43 -05:00
|
|
|
/** @type BasicElement */
|
2023-01-23 09:07:55 -05:00
|
|
|
export const Branch = (props) => {
|
2023-01-24 10:13:43 -05:00
|
|
|
/** @type Binding */ const stage = React.useState(
|
|
|
|
/** @type Stage */ ({ Open: false, Done: true }),
|
|
|
|
);
|
2023-01-23 09:07:55 -05:00
|
|
|
/** @type Binding */ const stageParent = React.useContext(BranchContext);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2023-01-24 17:28:17 -05:00
|
|
|
const [{ Open, Done }] = stageParent;
|
|
|
|
const [, Shut] = stage;
|
2023-01-24 10:13:43 -05:00
|
|
|
if (!Open && Done) {
|
|
|
|
Shut({ Open: false, Done: true });
|
2023-01-23 09:07:55 -05:00
|
|
|
}
|
|
|
|
}, [stageParent]);
|
|
|
|
|
2023-01-24 10:13:43 -05:00
|
|
|
return React.createElement(BranchContext.Provider, {
|
|
|
|
value: stage,
|
|
|
|
children: props.children,
|
|
|
|
});
|
2023-01-23 09:07:55 -05:00
|
|
|
};
|
|
|
|
|
2023-01-24 10:13:43 -05:00
|
|
|
/** @type BasicElement */
|
|
|
|
export const Button = (props) => {
|
2023-01-23 09:07:55 -05:00
|
|
|
/** @type Binding */
|
|
|
|
const [stageGet, stageSet] = React.useContext(BranchContext);
|
|
|
|
|
|
|
|
const handler = () => {
|
2023-01-24 10:13:43 -05:00
|
|
|
const value = { Open: !stageGet.Open, Done: false };
|
2023-01-23 09:07:55 -05:00
|
|
|
stageSet(value);
|
|
|
|
console.log(value);
|
|
|
|
};
|
|
|
|
|
2023-01-24 17:28:17 -05:00
|
|
|
return html`<button onClick=${handler} class=${props.class}>stage: ${
|
2023-01-24 10:13:43 -05:00
|
|
|
JSON.stringify(stageGet)
|
|
|
|
}
|
|
|
|
${props.children}
|
|
|
|
</button>`;
|
|
|
|
};
|
|
|
|
|
|
|
|
/** @type BasicElement */
|
|
|
|
export const Menu = (props) => {
|
|
|
|
const [stageGet, stageSet] = React.useContext(BranchContext);
|
|
|
|
const refElement = React.useRef(null);
|
2023-01-24 17:28:17 -05:00
|
|
|
/** @type {React.MutableRefObject<null|Toggler>} */
|
2023-01-24 12:01:10 -05:00
|
|
|
const refCollapser = React.useRef(null);
|
2023-01-24 10:13:43 -05:00
|
|
|
|
|
|
|
React.useEffect(() => {
|
2023-01-24 17:28:17 -05:00
|
|
|
refElement.current &&
|
|
|
|
(refCollapser.current = Collapser(refElement.current));
|
2023-01-24 12:01:10 -05:00
|
|
|
}, []);
|
2023-01-24 10:13:43 -05:00
|
|
|
|
2023-01-24 12:01:10 -05:00
|
|
|
React.useEffect(() => {
|
2023-01-24 17:28:17 -05:00
|
|
|
const instant = stageGet.Open == false && stageGet.Done;
|
|
|
|
refCollapser.current && refCollapser.current(
|
2023-01-24 12:01:10 -05:00
|
|
|
stageGet.Open,
|
2023-01-24 17:28:17 -05:00
|
|
|
instant ? 0 : 600,
|
2023-01-24 12:01:10 -05:00
|
|
|
() => stageSet({ ...stageGet, Done: true }),
|
|
|
|
);
|
2023-01-24 10:13:43 -05:00
|
|
|
}, [stageGet.Open]);
|
|
|
|
|
|
|
|
return html`
|
2023-01-24 17:28:17 -05:00
|
|
|
<div ref=${refElement} class=${props.class}>
|
2023-01-24 10:13:43 -05:00
|
|
|
${props.children}
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
};
|
|
|
|
|
2023-01-24 12:01:10 -05:00
|
|
|
/** @typedef {(Open:boolean)=>void} UserDone */
|
|
|
|
/** @typedef {(inOpen:boolean, inMilliseconds:number, inUserDone:UserDone)=>void} Toggler */
|
2023-01-24 17:28:17 -05:00
|
|
|
/** @typedef {(inElement:HTMLElement)=>Toggler} Collapse */
|
|
|
|
/** @type Collapse */
|
2023-01-24 12:01:10 -05:00
|
|
|
const Collapser = (inElement) => {
|
|
|
|
/** @type UserDone */
|
|
|
|
let userDone = () => {};
|
2023-01-24 17:28:17 -05:00
|
|
|
let userMode = false;
|
2023-01-24 10:13:43 -05:00
|
|
|
const collapse = inElement;
|
|
|
|
/** @type {(inEvent:TransitionEvent)=>void} */
|
|
|
|
const done = (inEvent) => {
|
|
|
|
if (inEvent.propertyName == "height" && inEvent.target == collapse) {
|
|
|
|
inEvent.stopPropagation();
|
2023-01-24 17:28:17 -05:00
|
|
|
if (userMode) {
|
2023-01-24 10:13:43 -05:00
|
|
|
collapse.style.height = "auto";
|
|
|
|
collapse.style.overflow = "visible";
|
2023-01-24 12:01:10 -05:00
|
|
|
userDone(true);
|
2023-01-24 10:13:43 -05:00
|
|
|
} else {
|
2023-01-24 12:01:10 -05:00
|
|
|
userDone(false);
|
2023-01-24 10:13:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/** @type Toggler */
|
2023-01-24 12:01:10 -05:00
|
|
|
const show = (inOpen, inMs, inDone) => {
|
2023-01-24 17:28:17 -05:00
|
|
|
userMode = inOpen;
|
2023-01-24 10:13:43 -05:00
|
|
|
collapse.removeEventListener("transitionend", done);
|
2023-01-24 12:01:10 -05:00
|
|
|
userDone = inDone;
|
2023-01-24 10:13:43 -05:00
|
|
|
collapse.addEventListener("transitionend", done);
|
|
|
|
|
|
|
|
collapse.style.height = collapse.clientHeight + "px";
|
|
|
|
collapse.style.overflow = "hidden";
|
|
|
|
collapse.style.transition = "none";
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
collapse.style.height = `${inOpen ? collapse.scrollHeight : 0}px`;
|
|
|
|
collapse.style.transition = `height ${inMs / 1000}s`;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
return show;
|
2023-01-23 09:07:55 -05:00
|
|
|
};
|