439 lines
19 KiB
JavaScript
439 lines
19 KiB
JavaScript
import van from "./van.js";
|
||
// Quote all tag names so that they're not mangled by minifier
|
||
const { "button": button, "div": div, "header": header, "input": input, "label": label, "span": span, "style": style } = van.tags;
|
||
const toStyleStr = (style) => Object.entries(style).map(([k, v]) => `${k}: ${v};`).join("");
|
||
const toStyleSheet = (styles) => {
|
||
return Object.entries(styles)
|
||
.map(([selector, props]) => `${selector} { ${toStyleStr(props)} }`)
|
||
.join("\n");
|
||
};
|
||
const stateProto = Object.getPrototypeOf(van.state(null));
|
||
const stateOf = (v) => (Object.getPrototypeOf(v ?? 0) === stateProto ? v : van.state(v));
|
||
export const Await = ({ value, container = div, Loading, Error }, children) => {
|
||
const data = van.state({ status: "pending" });
|
||
value
|
||
.then(result => data.val = { status: "fulfilled", value: result })
|
||
.catch(err => data.val = { status: "rejected", value: err });
|
||
return container(() => data.val.status === "pending" ? Loading?.() ?? "" :
|
||
data.val.status === "rejected" ? Error?.(data.val.value) :
|
||
children(data.val.value));
|
||
};
|
||
export const Modal = ({ closed, backgroundColor = "rgba(0,0,0,.5)", blurBackground = false, backgroundClass = "", backgroundStyleOverrides = {}, modalClass = "", modalStyleOverrides = {}, }, ...children) => {
|
||
const backgroundStyle = {
|
||
display: "flex",
|
||
"align-items": "center",
|
||
"justify-content": "center",
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
position: "fixed",
|
||
"z-index": 10000,
|
||
"background-color": backgroundColor,
|
||
"backdrop-filter": blurBackground ? "blur(0.25rem)" : "none",
|
||
...backgroundStyleOverrides,
|
||
};
|
||
const modalStyle = {
|
||
"border-radius": "0.5rem",
|
||
padding: "1rem",
|
||
display: "block",
|
||
"background-color": "white",
|
||
...modalStyleOverrides,
|
||
};
|
||
return () => closed.val ? null : div({ class: backgroundClass, style: toStyleStr(backgroundStyle) }, div({ class: modalClass, style: toStyleStr(modalStyle) }, children));
|
||
};
|
||
let tabsId = 0;
|
||
export const Tabs = ({ activeTab, resultClass = "", style = "", tabButtonRowColor = "#f1f1f1", tabButtonBorderStyle = "1px solid #000", tabButtonHoverColor = "#ddd", tabButtonActiveColor = "#ccc", transitionSec = 0.3, tabButtonRowClass = "", tabButtonRowStyleOverrides = {}, tabButtonClass = "", tabButtonStyleOverrides = {}, tabContentClass = "", tabContentStyleOverrides = {}, }, contents) => {
|
||
const activeTabState = activeTab ?? van.state(Object.keys(contents)[0]);
|
||
const tabButtonRowStylesStr = toStyleStr({
|
||
overflow: "hidden",
|
||
"background-color": tabButtonRowColor,
|
||
...tabButtonRowStyleOverrides,
|
||
});
|
||
const tabButtonStylesStr = toStyleStr({
|
||
float: "left",
|
||
border: "none",
|
||
"border-right": tabButtonBorderStyle,
|
||
outline: "none",
|
||
cursor: "pointer",
|
||
padding: "8px 16px",
|
||
transition: `background-color ${transitionSec}s`,
|
||
...tabButtonStyleOverrides,
|
||
});
|
||
const tabContentStylesStr = toStyleStr({
|
||
padding: "6px 12px",
|
||
"border-top": "none",
|
||
...tabContentStyleOverrides,
|
||
});
|
||
const id = "vanui-tabs-" + (++tabsId);
|
||
document.head.appendChild(van.tags["style"](`#${id} .vanui-tab-button { background-color: inherit }
|
||
#${id} .vanui-tab-button:hover { background-color: ${tabButtonHoverColor} }
|
||
#${id} .vanui-tab-button.active { background-color: ${tabButtonActiveColor} }`));
|
||
return div({ id, class: resultClass, style }, div({ class: tabButtonRowClass, style: tabButtonRowStylesStr }, Object.keys(contents).map(k => button({
|
||
class: () => ["vanui-tab-button"].concat(tabButtonClass ? tabButtonClass : [], k === activeTabState.val ? "active" : []).join(" "),
|
||
style: tabButtonStylesStr,
|
||
onclick: () => activeTabState.val = k,
|
||
}, k))), Object.entries(contents).map(([k, v]) => div({
|
||
class: tabContentClass,
|
||
style: () => `display: ${k === activeTabState.val ? "block" : "none"}; ${tabContentStylesStr}`,
|
||
}, v)));
|
||
};
|
||
export const Toggle = ({ on = false, size = 1, cursor = "pointer", offColor = "#ccc", onColor = "#2196F3", circleColor = "white", toggleClass = "", toggleStyleOverrides = {}, sliderClass = "", sliderStyleOverrides = {}, circleClass = "", circleStyleOverrides = {}, circleWhenOnStyleOverrides = {}, }) => {
|
||
const onState = stateOf(on);
|
||
const toggleStylesStr = toStyleStr({
|
||
position: "relative",
|
||
display: "inline-block",
|
||
width: 1.76 * size + "rem",
|
||
height: size + "rem",
|
||
cursor,
|
||
...toggleStyleOverrides,
|
||
});
|
||
const inputStylesStr = toStyleStr({
|
||
opacity: 0,
|
||
width: 0,
|
||
height: 0,
|
||
position: "absolute",
|
||
"z-index": 10000, // Ensures the toggle clickable
|
||
});
|
||
const sliderStylesStr = toStyleStr({
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
transition: ".4s",
|
||
"border-radius": size + "rem",
|
||
...sliderStyleOverrides,
|
||
});
|
||
const circleStylesStr = toStyleStr({
|
||
position: "absolute",
|
||
height: 0.76 * size + "rem",
|
||
width: 0.76 * size + "rem",
|
||
left: 0.12 * size + "rem",
|
||
bottom: 0.12 * size + "rem",
|
||
"background-color": circleColor,
|
||
transition: ".4s",
|
||
"border-radius": "50%",
|
||
...circleStyleOverrides,
|
||
});
|
||
const circleStylesWhenOnStr = toStyleStr({
|
||
transform: `translateX(${0.76 * size}rem)`,
|
||
...circleWhenOnStyleOverrides,
|
||
});
|
||
return label({ class: toggleClass, style: toggleStylesStr }, input({ type: "checkbox", style: inputStylesStr, oninput: e => onState.val = e.target.checked }), span({
|
||
class: sliderClass,
|
||
style: () => `${sliderStylesStr}; background-color: ${onState.val ? onColor : offColor};`,
|
||
}, span({
|
||
class: circleClass,
|
||
style: () => circleStylesStr + (onState.val ? circleStylesWhenOnStr : ""),
|
||
})));
|
||
};
|
||
export class MessageBoard {
|
||
_fadeOutSec;
|
||
_messageClass;
|
||
_messageStylesStr;
|
||
_closerClass;
|
||
_closerStylesStr;
|
||
_dom;
|
||
constructor({ top = "unset", bottom = "unset", backgroundColor = "#333D", fontColor = "white", fadeOutSec = 0.3, boardClass = "", boardStyleOverrides = {}, messageClass = "", messageStyleOverrides = {}, closerClass = "", closerStyleOverrides = {}, }, parentDom = document.body) {
|
||
const boardStylesStr = toStyleStr({
|
||
display: "flex",
|
||
"flex-direction": "column",
|
||
"align-items": "center",
|
||
position: "fixed",
|
||
top,
|
||
bottom,
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
"z-index": 10000,
|
||
...boardStyleOverrides,
|
||
});
|
||
this._fadeOutSec = fadeOutSec;
|
||
this._messageClass = messageClass;
|
||
this._messageStylesStr = toStyleStr({
|
||
display: "flex",
|
||
"background-color": backgroundColor,
|
||
color: fontColor,
|
||
padding: "15px",
|
||
"margin-bottom": "10px",
|
||
"border-radius": "5px",
|
||
transition: `opacity ${fadeOutSec}s, transform ${fadeOutSec}s`,
|
||
...messageStyleOverrides,
|
||
});
|
||
this._closerClass = closerClass;
|
||
this._closerStylesStr = toStyleStr({
|
||
display: "flex",
|
||
"align-items": "center",
|
||
"margin-left": "10px",
|
||
cursor: "pointer",
|
||
...closerStyleOverrides
|
||
});
|
||
parentDom.appendChild(this._dom = div({ class: boardClass, style: boardStylesStr }));
|
||
}
|
||
show({ message, closer, durationSec, closed = van.state(false), }) {
|
||
const removed = van.state(false);
|
||
van.derive(() => setTimeout((v) => removed.val = v, this._fadeOutSec * 1000, closed.val));
|
||
const msgDom = div({ class: this._messageClass, style: this._messageStylesStr }, div(message), closer ? div({ class: this._closerClass, style: this._closerStylesStr, onclick: () => closed.val = true }, closer) : null);
|
||
van.derive(() => closed.val && (msgDom.style.opacity = "0", msgDom.style.transform = "translateY(-20px)"));
|
||
if (durationSec)
|
||
setTimeout(() => closed.val = true, durationSec * 1000);
|
||
van.add(this._dom, () => removed.val ? null : msgDom);
|
||
return msgDom;
|
||
}
|
||
remove() { this._dom.remove(); }
|
||
}
|
||
export const Tooltip = ({ text, show, width = "200px", backgroundColor = "#333D", fontColor = "white", fadeInSec = 0.3, tooltipClass = "", tooltipStyleOverrides = {}, triangleClass = "", triangleStyleOverrides = {}, }) => {
|
||
const tooltipStylesStr = toStyleStr({
|
||
width,
|
||
visibility: "hidden",
|
||
"background-color": backgroundColor,
|
||
color: fontColor,
|
||
"text-align": "center",
|
||
padding: "5px",
|
||
"border-radius": "5px",
|
||
position: "absolute",
|
||
"z-index": 1,
|
||
bottom: "125%",
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
opacity: 0,
|
||
transition: `opacity ${fadeInSec}s`,
|
||
...tooltipStyleOverrides,
|
||
});
|
||
const triangleStylesStr = toStyleStr({
|
||
width: 0,
|
||
height: 0,
|
||
"margin-left": "-5px",
|
||
"border-left": "5px solid transparent",
|
||
"border-right": "5px solid transparent",
|
||
"border-top": "5px solid #333",
|
||
position: "absolute",
|
||
bottom: "-5px",
|
||
left: "50%",
|
||
...triangleStyleOverrides,
|
||
});
|
||
const dom = span({ class: tooltipClass, style: tooltipStylesStr }, text, div({ class: triangleClass, style: triangleStylesStr }));
|
||
van.derive(() => show.val ?
|
||
(dom.style.opacity = "1", dom.style.visibility = "visible") :
|
||
(dom.style.opacity = "0", dom.style.visibility = "hidden"));
|
||
return dom;
|
||
};
|
||
let optionGroupId = 0;
|
||
export const OptionGroup = ({ selected, normalColor = "#e2eef7", hoverColor = "#c1d4e9", selectedColor = "#90b6d9", selectedHoverColor = "#7fa5c8", fontColor = "black", transitionSec = 0.3, optionGroupClass = "", optionGroupStyleOverrides = {}, optionClass = "", optionStyleOverrides = {}, }, options) => {
|
||
const buttonGroupStylesStr = toStyleStr({
|
||
display: "flex",
|
||
...optionGroupStyleOverrides,
|
||
});
|
||
const buttonStylesStr = toStyleStr({
|
||
padding: "10px 20px",
|
||
border: "none",
|
||
color: fontColor,
|
||
cursor: "pointer",
|
||
outline: "none",
|
||
transition: `background-color ${transitionSec}s`,
|
||
...optionStyleOverrides,
|
||
});
|
||
const id = "vanui-option-group-" + (++optionGroupId);
|
||
document.head.appendChild(van.tags["style"](`#${id} .vanui-button { background-color: ${normalColor} }
|
||
#${id} .vanui-button:hover { background-color: ${hoverColor} }
|
||
#${id} .vanui-button.selected { background-color: ${selectedColor} }
|
||
#${id} .vanui-button.selected:hover { background-color: ${selectedHoverColor} }`));
|
||
return div({ id, class: optionGroupClass, style: buttonGroupStylesStr }, options.map(o => button({
|
||
class: () => ["vanui-button"].concat(optionClass ? optionClass : [], o === selected.val ? "selected" : []).join(" "),
|
||
style: buttonStylesStr,
|
||
onclick: () => selected.val = o,
|
||
}, o)));
|
||
};
|
||
export const Banner = ({ backgroundColor = "#fff1a8", fontColor = "currentcolor", sticky = false, bannerClass = "", bannerStyleOverrides = {}, }, ...children) => {
|
||
const bannerStyleStr = toStyleStr({
|
||
"background-color": backgroundColor,
|
||
color: fontColor,
|
||
top: 0,
|
||
position: sticky ? "sticky" : "static",
|
||
"z-index": 10,
|
||
...bannerStyleOverrides,
|
||
});
|
||
return header({ class: bannerClass, style: bannerStyleStr }, children);
|
||
};
|
||
let curWindowZIndex = 0;
|
||
export const topMostZIndex = () => ++curWindowZIndex;
|
||
export const FloatingWindow = ({ title, closed = van.state(false), x = 100, y = 100, width = 300, height = 200, closeCross = "×", customStacking = false, zIndex = 1, disableMove = false, disableResize = false, headerColor = "lightgray", windowClass = "", windowStyleOverrides = {}, headerClass = "", headerStyleOverrides = {}, childrenContainerClass = "", childrenContainerStyleOverrides = {}, crossClass = "", crossStyleOverrides = {}, crossHoverClass = "", crossHoverStyleOverrides = {}, }, ...children) => {
|
||
const xState = stateOf(x), yState = stateOf(y);
|
||
const widthState = stateOf(width), heightState = stateOf(height);
|
||
const zIndexState = stateOf(zIndex);
|
||
if (!customStacking)
|
||
zIndexState.val = topMostZIndex();
|
||
const dragging = van.state(false), resizingDirection = van.state(null);
|
||
const startX = van.state(0), startY = van.state(0);
|
||
const startWidth = van.state(0), startHeight = van.state(0);
|
||
const crossHover = crossHoverClass || Object.keys(crossHoverStyleOverrides) ?
|
||
van.state(false) : null;
|
||
const onmousedown = (e) => {
|
||
if (e.button !== 0)
|
||
return;
|
||
dragging.val = true;
|
||
startX.val = e.clientX;
|
||
startY.val = e.clientY;
|
||
document.body.style.userSelect = "none";
|
||
};
|
||
const onResizeMouseDown = (direction) => (e) => {
|
||
resizingDirection.val = direction;
|
||
startX.val = e.clientX;
|
||
startY.val = e.clientY;
|
||
startWidth.val = widthState.val;
|
||
startHeight.val = heightState.val;
|
||
document.body.style.userSelect = "none";
|
||
};
|
||
const onMouseMove = (e) => {
|
||
if (dragging.val) {
|
||
xState.val += e.clientX - startX.val;
|
||
yState.val += e.clientY - startY.val;
|
||
startX.val = e.clientX;
|
||
startY.val = e.clientY;
|
||
}
|
||
else if (resizingDirection.val) {
|
||
const deltaX = e.clientX - startX.val;
|
||
const deltaY = e.clientY - startY.val;
|
||
if (resizingDirection.val.includes("right"))
|
||
widthState.val = startWidth.val + deltaX;
|
||
if (resizingDirection.val.includes("bottom"))
|
||
heightState.val = startHeight.val + deltaY;
|
||
}
|
||
};
|
||
const onMouseUp = () => {
|
||
dragging.val = false;
|
||
resizingDirection.val = null;
|
||
document.body.style.userSelect = "";
|
||
};
|
||
document.addEventListener("mousemove", onMouseMove);
|
||
document.addEventListener("mouseup", onMouseUp);
|
||
const grabAreaBgColor = "transparent";
|
||
if (!document.getElementById("vanui-window-style")) {
|
||
const staticStyles = style({ type: "text/css", id: "vanui-window-style" }, toStyleSheet({
|
||
".vanui-window": {
|
||
position: "fixed",
|
||
"background-color": "white",
|
||
border: "1px solid black",
|
||
"border-radius": "0.5rem",
|
||
overflow: "hidden",
|
||
},
|
||
".vanui-window-dragarea": {
|
||
cursor: "move",
|
||
position: "absolute",
|
||
left: "0",
|
||
top: "0",
|
||
width: "100%",
|
||
height: "1rem",
|
||
},
|
||
".vanui-window-resize-right": {
|
||
cursor: "e-resize",
|
||
position: "absolute",
|
||
right: "0",
|
||
top: "0",
|
||
width: "10px",
|
||
height: "100%",
|
||
"background-color": grabAreaBgColor,
|
||
},
|
||
".vanui-window-resize-bottom": {
|
||
cursor: "s-resize",
|
||
position: "absolute",
|
||
left: "0",
|
||
bottom: "0",
|
||
width: "100%",
|
||
height: "10px",
|
||
"background-color": grabAreaBgColor,
|
||
},
|
||
".vanui-window-resize-rightbottom": {
|
||
cursor: "se-resize",
|
||
position: "absolute",
|
||
right: "0",
|
||
bottom: "0",
|
||
width: "10px",
|
||
height: "10px",
|
||
"background-color": grabAreaBgColor,
|
||
},
|
||
".vanui-window-header": {
|
||
cursor: "move",
|
||
"user-select": "none",
|
||
display: "flex",
|
||
"justify-content": "space-between",
|
||
"align-items": "center",
|
||
padding: "0.5rem",
|
||
},
|
||
".vanui-window-cross": {
|
||
cursor: "pointer",
|
||
"font-family": "Arial",
|
||
transition: "background-color 0.3s, color 0.3s",
|
||
"border-radius": "50%",
|
||
width: "24px",
|
||
height: "24px",
|
||
display: "flex",
|
||
"align-items": "center",
|
||
"justify-content": "center",
|
||
},
|
||
".vanui-window-cross:hover": {
|
||
"background-color": "red",
|
||
color: "white",
|
||
},
|
||
".vanui-window-children": {
|
||
padding: "0.5rem",
|
||
}
|
||
}));
|
||
document.head.appendChild(staticStyles);
|
||
}
|
||
return () => closed.val ? null : div({
|
||
class: ["vanui-window"].concat(windowClass ? windowClass : []).join(" "),
|
||
style: () => toStyleStr({
|
||
left: `${xState.val}px`,
|
||
top: `${yState.val}px`,
|
||
width: `${widthState.val}px`,
|
||
height: `${heightState.val}px`,
|
||
"z-index": zIndexState.val,
|
||
...windowStyleOverrides,
|
||
}),
|
||
...(customStacking ? {} : { onmousedown: () => zIndexState.val = topMostZIndex() }),
|
||
}, title ? header({
|
||
class: ["vanui-window-header"].concat(headerClass ? headerClass : []).join(" "),
|
||
style: toStyleStr({
|
||
"background-color": headerColor,
|
||
...(disableMove ? { cursor: "auto" } : {}),
|
||
...headerStyleOverrides,
|
||
}),
|
||
...(disableMove ? {} : { onmousedown }),
|
||
}, title, closeCross ? span({
|
||
class: () => ["vanui-window-cross"]
|
||
.concat(crossClass ? crossClass : [])
|
||
.concat(crossHoverClass && crossHover.val ? crossHoverClass : [])
|
||
.join(" "),
|
||
style: () => toStyleStr({
|
||
...crossStyleOverrides,
|
||
...(Object.keys(crossHoverStyleOverrides).length && crossHover.val ?
|
||
crossHoverStyleOverrides : {}),
|
||
}),
|
||
onclick: () => closed.val = true,
|
||
...(crossHover ? {
|
||
onmouseenter: () => crossHover.val = true,
|
||
onmouseleave: () => crossHover.val = false,
|
||
} : {})
|
||
}, closeCross) : null) : disableMove ? null : div({
|
||
class: "vanui-window-dragarea",
|
||
onmousedown,
|
||
}), disableResize ? [] : [
|
||
div({
|
||
class: "vanui-window-resize-right",
|
||
onmousedown: onResizeMouseDown("right"),
|
||
}),
|
||
div({
|
||
class: "vanui-window-resize-bottom",
|
||
onmousedown: onResizeMouseDown("bottom"),
|
||
}),
|
||
div({
|
||
class: "vanui-window-resize-rightbottom",
|
||
onmousedown: onResizeMouseDown("rightbottom"),
|
||
}),
|
||
], div({
|
||
class: ["vanui-window-children"].concat(childrenContainerClass ? childrenContainerClass : []).join(" "),
|
||
style: toStyleStr(childrenContainerStyleOverrides)
|
||
}, children));
|
||
};
|