new-tree-system/js/tree.js

143 lines
4.4 KiB
JavaScript
Raw Normal View History

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-02-06 14:15:00 -05:00
/** @typedef {{Open:boolean, Done:boolean, Away:boolean}} Stage*/
2023-01-23 09:07:55 -05:00
/** @typedef {[set:Stage, get:React.StateUpdater<Stage>]} Binding */
const BranchContext = React.createContext(
2023-02-06 14:15:00 -05:00
/** @type Binding */ ([{ Open: false, Done: true, Away: false }, (_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(
2023-02-06 14:15:00 -05:00
/** @type Stage */ ({ Open: false, Done: true, Away: false }),
2023-01-24 10:13:43 -05:00
);
2023-01-23 09:07:55 -05:00
/** @type Binding */ const stageParent = React.useContext(BranchContext);
React.useEffect(() => {
2023-02-06 14:15:00 -05:00
const [{ Open, Done, Away }] = stageParent;
2023-01-24 17:28:17 -05:00
const [, Shut] = stage;
2023-02-09 10:56:25 -05:00
2023-01-24 10:13:43 -05:00
if (!Open && Done) {
2023-02-06 14:15:00 -05:00
Shut({ Open: false, Done: true, Away });
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);
2023-02-06 14:15:00 -05:00
const handleClick = () => {
console.log("set: open");
stageSet((state) => ({ ...state, Open: !state.Open, Done: false }));
2023-01-23 09:07:55 -05:00
};
2023-02-09 10:56:25 -05:00
return html`<button onClick=${handleClick} 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-02-09 10:56:25 -05:00
const instant = stageGet.Open == false && stageGet.Done;
refCollapser.current && refCollapser.current(
stageGet.Open,
instant ? 0 : 600,
() => stageSet({ ...stageGet, Done: true }),
);
/** @type React.MutableRefObject<Handler|null> */
const refHandler = React.useRef(null);
refHandler.current = (away) => {
console.log("set: away");
stageSet((state) => ({ ...state, Away: away }));
};
useAway(refElement, refHandler);
2023-01-24 10:13:43 -05:00
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 */
2023-02-09 10:56:25 -05:00
/** @typedef {(inOpen:boolean, inMilliseconds:number, inUserDone:UserDone)=>void | (()=>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-02-09 10:56:25 -05:00
/** @type number */ let frameRequest;
2023-01-24 10:13:43 -05:00
/** @type {(inEvent:TransitionEvent)=>void} */
const done = (inEvent) => {
2023-02-09 10:56:25 -05:00
if (inEvent.propertyName == "height" && inEvent.target == inElement) {
2023-01-24 10:13:43 -05:00
inEvent.stopPropagation();
2023-01-24 17:28:17 -05:00
if (userMode) {
2023-02-09 10:56:25 -05:00
inElement.style.height = "auto";
inElement.style.overflow = "visible";
2023-01-24 10:13:43 -05:00
}
2023-02-09 10:56:25 -05:00
userDone(userMode);
2023-01-24 10:13:43 -05:00
}
};
2023-02-09 10:56:25 -05:00
inElement.addEventListener("transitionend", done);
2023-01-24 10:13:43 -05:00
/** @type Toggler */
2023-01-24 12:01:10 -05:00
const show = (inOpen, inMs, inDone) => {
2023-02-09 10:56:25 -05:00
cancelAnimationFrame(frameRequest);
if ((!inOpen && !inMs) && !inDone) {
inElement.removeEventListener("transitionend", done);
} else {
userDone = inDone;
userMode = inOpen;
inElement.style.height = inElement.clientHeight + "px";
inElement.style.overflow = "hidden";
inElement.style.transition = "none";
frameRequest = requestAnimationFrame(() => {
inElement.style.height = `${inOpen ? inElement.scrollHeight : 0}px`;
inElement.style.transition = `height ${inMs / 1000}s`;
});
}
2023-01-24 10:13:43 -05:00
};
return show;
2023-01-23 09:07:55 -05:00
};
2023-01-24 23:54:25 -05:00
2023-02-06 14:15:00 -05:00
/** @typedef {(away:boolean, e:Event)=>void} Handler */
/** @type {(inRef:React.MutableRefObject<HTMLElement|null>, inHandler:React.MutableRefObject<Handler|null>)=>void} */
2023-02-09 10:56:25 -05:00
export const useAway = (inRef, inHandlerRef) => {
2023-02-06 14:15:00 -05:00
/** @type React.MutableRefObject<(e:Event)=>void> */
2023-01-24 23:54:25 -05:00
const handlerClick = React.useRef((e) => {
const index = inRef.current ? e.composedPath().indexOf(inRef.current) : -1;
2023-02-09 10:56:25 -05:00
inHandlerRef.current && inHandlerRef.current(index < 0, e);
2023-01-24 23:54:25 -05:00
});
React.useEffect(() => {
document.addEventListener("click", handlerClick.current);
return () => document.removeEventListener("click", handlerClick.current);
2023-02-09 10:56:25 -05:00
}, []);
2023-01-24 23:54:25 -05:00
};