1180 lines
46 KiB
JavaScript
1180 lines
46 KiB
JavaScript
/*
|
|
Sam Russell
|
|
Captured Sun
|
|
12.26.25 - Fixing stack state problem
|
|
12.25.25 - State for arrays, nested objects. State for stacks (Shadow-only)
|
|
12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs.
|
|
12.16.25 - [comalyr] - State
|
|
11.25.25.1 - Added minHeight and minWidth to be counted as numerical styles
|
|
11.25.25 - Added onChange for check boxes, added setQuery / onQueryChanged for easy filtering
|
|
11.24.25 - Fixing onClick because it was reversed, adding event to onHover params
|
|
11.23.25 - Added onSubmit() event for form submission, added marginHorizontal() and marginVertical()
|
|
11.20.25 - Added "pct" style unit, added alignVertical and alignHorizontal for flex boxes
|
|
11.19.25 - Allowing for "auto" values in otherwise numeric styles, adding vmin and vmax units
|
|
11.17.25.3 - Adding styles() and fixing dynamic function from earlier
|
|
11.17.25.2 - Fixing onNavigate() and onAppear()
|
|
11.17.25 - Added dynamic function to have units in style func parameters.
|
|
11.14.25 - Added onTouch, onTap. Changed style setters to work with Safari. Added center() funcs.
|
|
11.13.25 - changed onFocus() to be a boolean event, added onInput()
|
|
11.9.25 - changed p(innerText) to p(innerHTML), adjusted onNavigate to work for multiple elements and with correct "this" scope
|
|
11.7.25 - changed registerShadow() to register(), changed onClick() to be like onHover()
|
|
11.6.25 - adding default value for "button()" "children" parameter
|
|
10.29.25 - adding "gap()" and "label()" functions
|
|
*/
|
|
|
|
/* $ NAVIGATION */
|
|
let oldPushState = history.pushState;
|
|
history.pushState = function pushState() {
|
|
let ret = oldPushState.apply(this, arguments);
|
|
window.dispatchEvent(new Event('pushstate'));
|
|
window.dispatchEvent(new Event('navigate'));
|
|
return ret;
|
|
};
|
|
|
|
window.addEventListener('popstate', () => {
|
|
window.dispatchEvent(new Event('navigate'));
|
|
});
|
|
|
|
window.setQuery = function(key, value) {
|
|
const url = new URL(window.location.href);
|
|
const params = url.searchParams;
|
|
|
|
if (value === null || value === undefined) {
|
|
params.delete(key);
|
|
} else {
|
|
params.set(key, value);
|
|
}
|
|
|
|
const newUrl = url.toString();
|
|
history.replaceState(null, "", newUrl);
|
|
window.dispatchEvent(new Event('query-changed'));
|
|
|
|
return newUrl;
|
|
};
|
|
|
|
window.navigateTo = function(url) {
|
|
window.dispatchEvent(new Event('navigate'));
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
/* $ SELECTOR */
|
|
|
|
HTMLElement.prototype.$ = function(selector) {
|
|
return window.$(selector, this)
|
|
}
|
|
DocumentFragment.prototype.$ = function(selector) {
|
|
return window.$(selector, this)
|
|
}
|
|
window.$ = function(selector, el = document) {
|
|
return el.querySelector(selector)
|
|
}
|
|
|
|
window.$$ = function(selector, el = document) {
|
|
return Array.from(el.querySelectorAll(selector))
|
|
}
|
|
HTMLElement.prototype.$$ = function(selector) {
|
|
return window.$$(selector, this)
|
|
}
|
|
DocumentFragment.prototype.$$ = function(selector) {
|
|
return window.$$(selector, this)
|
|
}
|
|
|
|
/* CONSOLE */
|
|
|
|
console.red = function(message) {
|
|
this.log(`%c${message}`, "color: rgb(254, 79, 42);");
|
|
};
|
|
|
|
console.green = function(message) {
|
|
this.log(`%c${message}`, "color: rgb(79, 254, 42);");
|
|
|
|
}
|
|
|
|
/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
|
|
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
document.documentElement.classList.add(darkMode ? 'dark' : 'light');
|
|
|
|
window.getColor = function(name) {
|
|
const rootStyles = getComputedStyle(document.documentElement);
|
|
const color = rootStyles.getPropertyValue(`--${name}`).trim();
|
|
if(!color) {
|
|
throw new Error("Color not found")
|
|
}
|
|
return color
|
|
}
|
|
|
|
/* MOBILE */
|
|
|
|
window.isMobile = function isMobile() {
|
|
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
|
|
}
|
|
|
|
window.css = function css(cssString) {
|
|
let container = document.querySelector("style#pageStyle");
|
|
if(!container) {
|
|
container = document.createElement('style');
|
|
container.id = "pageStyle";
|
|
document.head.appendChild(container);
|
|
}
|
|
|
|
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
|
|
primarySelector = primarySelector.replace(/\*/g, "all");
|
|
primarySelector = primarySelector.replace(/#/g, "id-");
|
|
primarySelector = primarySelector.replace(/,/g, "");
|
|
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
|
|
if(!stylesheet) {
|
|
stylesheet = document.createElement('style');
|
|
stylesheet.id = primarySelector;
|
|
stylesheet.appendChild(document.createTextNode(cssString));
|
|
container.appendChild(stylesheet);
|
|
} else {
|
|
stylesheet.innerText = cssString
|
|
}
|
|
}
|
|
|
|
window.html = function html(elementString) {
|
|
let parser = new DOMParser();
|
|
let doc = parser.parseFromString(elementString, 'text/html');
|
|
return doc.body.firstChild;
|
|
}
|
|
|
|
window.util = {}
|
|
window.util.observeClassChange = (el, callback) => {
|
|
if (!el || !(el instanceof Element)) {
|
|
throw new Error("observeClassChange requires a valid DOM element.");
|
|
}
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === "attributes" && mutation.attributeName === "class") {
|
|
callback(el.classList);
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe(el, {
|
|
attributes: true,
|
|
attributeFilter: ["class"]
|
|
});
|
|
|
|
return observer; // Optional: return it so you can disconnect later
|
|
}
|
|
|
|
/* PAGE SETUP */
|
|
|
|
Object.defineProperty(Array.prototype, 'last', {
|
|
get() {
|
|
return this[this.length - 1];
|
|
},
|
|
enumerable: false,
|
|
});
|
|
|
|
/* QUILL */
|
|
|
|
window.quill = {
|
|
rendering: [],
|
|
lastState: null,
|
|
|
|
render: (el) => {
|
|
if(el instanceof Shadow) {
|
|
let parent = quill.rendering[quill.rendering.length-1]
|
|
if(!parent) {
|
|
parent = document.body
|
|
}
|
|
parent.appendChild(el)
|
|
} else {
|
|
if(!el.render) {el.render = () => {}}
|
|
let parent = quill.rendering[quill.rendering.length-1]
|
|
if(!parent) throw new Error("Quill: no parent for element")
|
|
parent.appendChild(el)
|
|
}
|
|
|
|
quill.rendering.push(el)
|
|
el.render()
|
|
quill.rendering.pop(el)
|
|
},
|
|
|
|
rerender: (el) => {
|
|
Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
|
|
el.innerHTML = ""
|
|
el.removeAllListeners()
|
|
|
|
quill.rendering.push(el)
|
|
el.render()
|
|
quill.rendering.pop()
|
|
},
|
|
|
|
rerenderStackContents: (el, cb) => {
|
|
el.innerHTML = ""
|
|
quill.rendering.push(el)
|
|
cb()
|
|
quill.rendering.pop()
|
|
},
|
|
|
|
isStack: (el) => {
|
|
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
|
|
},
|
|
}
|
|
|
|
window.Shadow = class extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
}
|
|
}
|
|
|
|
window.register = (el, tagname) => {
|
|
if (typeof el.prototype.render !== 'function') {
|
|
throw new Error("Element must have a render: " + el.prototype.constructor.name)
|
|
}
|
|
if(!tagname) {
|
|
tagname = el.prototype.constructor.name.toLowerCase() + "-"
|
|
}
|
|
customElements.define(tagname, el)
|
|
if(el.css) {
|
|
css(el.css)
|
|
}
|
|
|
|
window[el.prototype.constructor.name] = function (...params) {
|
|
let instance = new el(...params)
|
|
if(instance.state) {
|
|
const proxyCache = new WeakMap();
|
|
|
|
function reactive(value, path=[]) {
|
|
if (value && typeof value === "object") {
|
|
if (proxyCache.has(value)) return proxyCache.get(value);
|
|
|
|
const p = new Proxy(value, createHandlers(path));
|
|
proxyCache.set(value, p);
|
|
return p;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function isNumericKey(prop) {
|
|
return typeof prop === "string" && prop !== "" && String(+prop) === prop;
|
|
}
|
|
|
|
function createHandlers(path) {
|
|
return {
|
|
get(target, prop, receiver) {
|
|
if (typeof prop === "symbol") {
|
|
return Reflect.get(target, prop, receiver);
|
|
}
|
|
|
|
let nextPath = (Array.isArray(target) && !isNumericKey(prop)) ? path : path.concat(prop) // To filter out arr.length, arr.map, arr.forEach, etc.
|
|
quill.lastState = nextPath.join(".");
|
|
|
|
const v = Reflect.get(target, prop, receiver);
|
|
return reactive(v, nextPath);
|
|
},
|
|
|
|
set(target, prop, value, receiver) {
|
|
const oldLength = Array.isArray(target) ? target.length : undefined;
|
|
const oldValue = target[prop];
|
|
if (oldValue === value) return true;
|
|
|
|
const result = Reflect.set(target, prop, value, receiver);
|
|
|
|
let changedPath = (Array.isArray(target) && (!isNumericKey(prop) || target.length !== oldLength)) ? path : path.concat(prop).join("."); // To filter out arr.length, arr.map, arr.forEach, and also a push/pop/unshift.
|
|
const watchers = instance.stateWatchers[changedPath];
|
|
|
|
if (watchers) {
|
|
watchers.forEach(cb => cb());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
|
|
let proxy = reactive(instance.state)
|
|
|
|
Object.defineProperty(instance, "state", {
|
|
value: proxy,
|
|
writable: false,
|
|
configurable: false,
|
|
enumerable: true
|
|
});
|
|
|
|
let stateWatchers = {}
|
|
Object.keys(instance.state).forEach((key) => stateWatchers[key] = [])
|
|
Object.defineProperty(instance, "stateWatchers", {
|
|
value: stateWatchers,
|
|
writable: false,
|
|
configurable: false,
|
|
enumerable: true
|
|
});
|
|
}
|
|
quill.render(instance)
|
|
return instance
|
|
}
|
|
}
|
|
|
|
HTMLElement.prototype.rerender = function() {
|
|
quill.rerender(this)
|
|
}
|
|
|
|
/* Styling */
|
|
|
|
window.pct = "%"
|
|
window.vmin = "vmin"
|
|
window.vmax = "vmax"
|
|
window.vh = "vh"
|
|
window.vw = "vw"
|
|
window.px = "px"
|
|
window.em = "em"
|
|
window.rem = "rem"
|
|
window.inches = "in"
|
|
|
|
HTMLElement.prototype.addStyle = function(func) {
|
|
return func(this)
|
|
}
|
|
|
|
window.css = function css(cssString) {
|
|
let container = document.querySelector("style#pageStyle");
|
|
if(!container) {
|
|
container = document.createElement('style');
|
|
container.id = "pageStyle";
|
|
document.head.appendChild(container);
|
|
}
|
|
|
|
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
|
|
primarySelector = primarySelector.replace(/\*/g, "all");
|
|
primarySelector = primarySelector.replace(/#/g, "id-");
|
|
primarySelector = primarySelector.replace(/,/g, "");
|
|
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
|
|
if(!stylesheet) {
|
|
stylesheet = document.createElement('style');
|
|
stylesheet.id = primarySelector;
|
|
stylesheet.appendChild(document.createTextNode(cssString));
|
|
container.appendChild(stylesheet);
|
|
} else {
|
|
stylesheet.innerText = cssString
|
|
}
|
|
}
|
|
|
|
function extendHTMLElementWithStyleSetters() {
|
|
|
|
function cssValueType(prop) {
|
|
const div = document.createElement("div");
|
|
const style = div.style;
|
|
if (!(prop in style)) return "invalid";
|
|
|
|
switch(prop) {
|
|
|
|
case "gap":
|
|
case "borderRadius":
|
|
case "width":
|
|
case "height":
|
|
case "maxWidth":
|
|
case "maxHeight":
|
|
case "minWidth":
|
|
case "minHeight":
|
|
|
|
case "left":
|
|
case "top":
|
|
case "bottom":
|
|
case "right":
|
|
|
|
case "padding":
|
|
case "paddingLeft":
|
|
case "paddingTop":
|
|
case "paddingBottom":
|
|
case "paddingRight":
|
|
|
|
case "margin":
|
|
case "marginLeft":
|
|
case "marginTop":
|
|
case "marginBottom":
|
|
case "marginRight":
|
|
|
|
case "textUnderlineOffset":
|
|
case "letterSpacing":
|
|
|
|
return "unit-number"
|
|
|
|
default:
|
|
|
|
return "string"
|
|
}
|
|
|
|
}
|
|
|
|
let allStyleProps = ["accentColor", "additiveSymbols", "alignContent", "alignItems", "alignSelf", "alignmentBaseline", "all", "anchorName", "anchorScope", "animation", "animationComposition", "animationDelay", "animationDirection", "animationDuration", "animationFillMode", "animationIterationCount", "animationName", "animationPlayState", "animationRange", "animationRangeEnd", "animationRangeStart", "animationTimeline", "animationTimingFunction", "appRegion", "appearance", "ascentOverride", "aspectRatio", "backdropFilter", "backfaceVisibility", "background", "backgroundAttachment", "backgroundBlendMode", "backgroundClip", "backgroundColor", "backgroundImage", "backgroundOrigin", "backgroundPosition", "backgroundPositionX", "backgroundPositionY", "backgroundRepeat", "backgroundSize", "basePalette", "baselineShift", "baselineSource", "blockSize", "border", "borderBlock", "borderBlockColor", "borderBlockEnd", "borderBlockEndColor", "borderBlockEndStyle", "borderBlockEndWidth", "borderBlockStart", "borderBlockStartColor", "borderBlockStartStyle", "borderBlockStartWidth", "borderBlockStyle", "borderBlockWidth", "borderBottom", "borderBottomColor", "borderBottomLeftRadius", "borderBottomRightRadius", "borderBottomStyle", "borderBottomWidth", "borderCollapse", "borderColor", "borderEndEndRadius", "borderEndStartRadius", "borderImage", "borderImageOutset", "borderImageRepeat", "borderImageSlice", "borderImageSource", "borderImageWidth", "borderInline", "borderInlineColor", "borderInlineEnd", "borderInlineEndColor", "borderInlineEndStyle", "borderInlineEndWidth", "borderInlineStart", "borderInlineStartColor", "borderInlineStartStyle", "borderInlineStartWidth", "borderInlineStyle", "borderInlineWidth", "borderLeft", "borderLeftColor", "borderLeftStyle", "borderLeftWidth", "borderRadius", "borderRight", "borderRightColor", "borderRightStyle", "borderRightWidth", "borderSpacing", "borderStartEndRadius", "borderStartStartRadius", "borderStyle", "borderTop", "borderTopColor", "borderTopLeftRadius", "borderTopRightRadius", "borderTopStyle", "borderTopWidth", "borderWidth", "bottom", "boxDecorationBreak", "boxShadow", "boxSizing", "breakAfter", "breakBefore", "breakInside", "bufferedRendering", "captionSide", "caretAnimation", "caretColor", "clear", "clip", "clipPath", "clipRule", "color", "colorInterpolation", "colorInterpolationFilters", "colorRendering", "colorScheme", "columnCount", "columnFill", "columnGap", "columnRule", "columnRuleColor", "columnRuleStyle", "columnRuleWidth", "columnSpan", "columnWidth", "columns", "contain", "containIntrinsicBlockSize", "containIntrinsicHeight", "containIntrinsicInlineSize", "containIntrinsicSize", "containIntrinsicWidth", "container", "containerName", "containerType", "content", "contentVisibility", "cornerBlockEndShape", "cornerBlockStartShape", "cornerBottomLeftShape", "cornerBottomRightShape", "cornerBottomShape", "cornerEndEndShape", "cornerEndStartShape", "cornerInlineEndShape", "cornerInlineStartShape", "cornerLeftShape", "cornerRightShape", "cornerShape", "cornerStartEndShape", "cornerStartStartShape", "cornerTopLeftShape", "cornerTopRightShape", "cornerTopShape", "counterIncrement", "counterReset", "counterSet", "cursor", "cx", "cy", "d", "descentOverride", "direction", "display", "dominantBaseline", "dynamicRangeLimit", "emptyCells", "fallback", "fieldSizing", "fill", "fillOpacity", "fillRule", "filter", "flex", "flexBasis", "flexDirection", "flexFlow", "flexGrow", "flexShrink", "flexWrap", "float", "floodColor", "floodOpacity", "font", "fontDisplay", "fontFamily", "fontFeatureSettings", "fontKerning", "fontOpticalSizing", "fontPalette", "fontSize", "fontSizeAdjust", "fontStretch", "fontStyle", "fontSynthesis", "fontSynthesisSmallCaps", "fontSynthesisStyle", "fontSynthesisWeight", "fontVariant", "fontVariantAlternates", "fontVariantCaps", "fontVariantEastAsian", "fontVariantEmoji", "fontVariantLigatures", "fontVariantNumeric", "fontVariantPosition", "fontVariationSettings", "fontWeight", "forcedColorAdjust", "gap", "grid", "gridArea", "gridAutoColumns", "gridAutoFlow", "gridAutoRows", "gridColumn", "gridColumnEnd", "gridColumnGap", "gridColumnStart", "gridGap", "gridRow", "gridRowEnd", "gridRowGap", "gridRowStart", "gridTemplate", "gridTemplateAreas", "gridTemplateColumns", "gridTemplateRows", "height", "hyphenateCharacter", "hyphenateLimitChars", "hyphens", "imageOrientation", "imageRendering", "inherits", "initialLetter", "initialValue", "inlineSize", "inset", "insetBlock", "insetBlockEnd", "insetBlockStart", "insetInline", "insetInlineEnd", "insetInlineStart", "interactivity", "interpolateSize", "isolation", "justifyContent", "justifyItems", "justifySelf", "left", "letterSpacing", "lightingColor", "lineBreak", "lineGapOverride", "lineHeight", "listStyle", "listStyleImage", "listStylePosition", "listStyleType", "margin", "marginBlock", "marginBlockEnd", "marginBlockStart", "marginBottom", "marginInline", "marginInlineEnd", "marginInlineStart", "marginLeft", "marginRight", "marginTop", "marker", "markerEnd", "markerMid", "markerStart", "mask", "maskClip", "maskComposite", "maskImage", "maskMode", "maskOrigin", "maskPosition", "maskRepeat", "maskSize", "maskType", "mathDepth", "mathShift", "mathStyle", "maxBlockSize", "maxHeight", "maxInlineSize", "maxWidth", "minBlockSize", "minHeight", "minInlineSize", "minWidth", "mixBlendMode", "navigation", "negative", "objectFit", "objectPosition", "objectViewBox", "offset", "offsetAnchor", "offsetDistance", "offsetPath", "offsetPosition", "offsetRotate", "opacity", "order", "orphans", "outline", "outlineColor", "outlineOffset", "outlineStyle", "outlineWidth", "overflow", "overflowAnchor", "overflowBlock", "overflowClipMargin", "overflowInline", "overflowWrap", "overflowX", "overflowY", "overlay", "overrideColors", "overscrollBehavior", "overscrollBehaviorBlock", "overscrollBehaviorInline", "overscrollBehaviorX", "overscrollBehaviorY", "pad", "padding", "paddingBlock", "paddingBlockEnd", "paddingBlockStart", "paddingBottom", "paddingInline", "paddingInlineEnd", "paddingInlineStart", "paddingLeft", "paddingRight", "paddingTop", "page", "pageBreakAfter", "pageBreakBefore", "pageBreakInside", "pageOrientation", "paintOrder", "perspective", "perspectiveOrigin", "placeContent", "placeItems", "placeSelf", "pointerEvents", "position", "positionAnchor", "positionArea", "positionTry", "positionTryFallbacks", "positionTryOrder", "positionVisibility", "prefix", "printColorAdjust", "quotes", "r", "range", "readingFlow", "readingOrder", "resize", "result", "right", "rotate", "rowGap", "rubyAlign", "rubyPosition", "rx", "ry", "scale", "scrollBehavior", "scrollInitialTarget", "scrollMargin", "scrollMarginBlock", "scrollMarginBlockEnd", "scrollMarginBlockStart", "scrollMarginBottom", "scrollMarginInline", "scrollMarginInlineEnd", "scrollMarginInlineStart", "scrollMarginLeft", "scrollMarginRight", "scrollMarginTop", "scrollMarkerGroup", "scrollPadding", "scrollPaddingBlock", "scrollPaddingBlockEnd", "scrollPaddingBlockStart", "scrollPaddingBottom", "scrollPaddingInline", "scrollPaddingInlineEnd", "scrollPaddingInlineStart", "scrollPaddingLeft", "scrollPaddingRight", "scrollPaddingTop", "scrollSnapAlign", "scrollSnapStop", "scrollSnapType", "scrollTargetGroup", "scrollTimeline", "scrollTimelineAxis", "scrollTimelineName", "scrollbarColor", "scrollbarGutter", "scrollbarWidth", "shapeImageThreshold", "shapeMargin", "shapeOutside", "shapeRendering", "size", "sizeAdjust", "speak", "speakAs", "src", "stopColor", "stopOpacity", "stroke", "strokeDasharray", "strokeDashoffset", "strokeLinecap", "strokeLinejoin", "strokeMiterlimit", "strokeOpacity", "strokeWidth", "suffix", "symbols", "syntax", "system", "tabSize", "tableLayout", "textAlign", "textAlignLast", "textAnchor", "textAutospace", "textBox", "textBoxEdge", "textBoxTrim", "textCombineUpright", "textDecoration", "textDecorationColor", "textDecorationLine", "textDecorationSkipInk", "textDecorationStyle", "textDecorationThickness", "textEmphasis", "textEmphasisColor", "textEmphasisPosition", "textEmphasisStyle", "textIndent", "textOrientation", "textOverflow", "textRendering", "textShadow", "textSizeAdjust", "textSpacingTrim", "textTransform", "textUnderlineOffset", "textUnderlinePosition", "textWrap", "textWrapMode", "textWrapStyle", "timelineScope", "top", "touchAction", "transform", "transformBox", "transformOrigin", "transformStyle", "transition", "transitionBehavior", "transitionDelay", "transitionDuration", "transitionProperty", "transitionTimingFunction", "translate", "types", "unicodeBidi", "unicodeRange", "userSelect", "vectorEffect", "verticalAlign", "viewTimeline", "viewTimelineAxis", "viewTimelineInset", "viewTimelineName", "viewTransitionClass", "viewTransitionGroup", "viewTransitionName", "visibility", "webkitAlignContent", "webkitAlignItems", "webkitAlignSelf", "webkitAnimation", "webkitAnimationDelay", "webkitAnimationDirection", "webkitAnimationDuration", "webkitAnimationFillMode", "webkitAnimationIterationCount", "webkitAnimationName", "webkitAnimationPlayState", "webkitAnimationTimingFunction", "webkitAppRegion", "webkitAppearance", "webkitBackfaceVisibility", "webkitBackgroundClip", "webkitBackgroundOrigin", "webkitBackgroundSize", "webkitBorderAfter", "webkitBorderAfterColor", "webkitBorderAfterStyle", "webkitBorderAfterWidth", "webkitBorderBefore", "webkitBorderBeforeColor", "webkitBorderBeforeStyle", "webkitBorderBeforeWidth", "webkitBorderBottomLeftRadius", "webkitBorderBottomRightRadius", "webkitBorderEnd", "webkitBorderEndColor", "webkitBorderEndStyle", "webkitBorderEndWidth", "webkitBorderHorizontalSpacing", "webkitBorderImage", "webkitBorderRadius", "webkitBorderStart", "webkitBorderStartColor", "webkitBorderStartStyle", "webkitBorderStartWidth", "webkitBorderTopLeftRadius", "webkitBorderTopRightRadius", "webkitBorderVerticalSpacing", "webkitBoxAlign", "webkitBoxDecorationBreak", "webkitBoxDirection", "webkitBoxFlex", "webkitBoxOrdinalGroup", "webkitBoxOrient", "webkitBoxPack", "webkitBoxReflect", "webkitBoxShadow", "webkitBoxSizing", "webkitClipPath", "webkitColumnBreakAfter", "webkitColumnBreakBefore", "webkitColumnBreakInside", "webkitColumnCount", "webkitColumnGap", "webkitColumnRule", "webkitColumnRuleColor", "webkitColumnRuleStyle", "webkitColumnRuleWidth", "webkitColumnSpan", "webkitColumnWidth", "webkitColumns", "webkitFilter", "webkitFlex", "webkitFlexBasis", "webkitFlexDirection", "webkitFlexFlow", "webkitFlexGrow", "webkitFlexShrink", "webkitFlexWrap", "webkitFontFeatureSettings", "webkitFontSmoothing", "webkitHyphenateCharacter", "webkitJustifyContent", "webkitLineBreak", "webkitLineClamp", "webkitLocale", "webkitLogicalHeight", "webkitLogicalWidth", "webkitMarginAfter", "webkitMarginBefore", "webkitMarginEnd", "webkitMarginStart", "webkitMask", "webkitMaskBoxImage", "webkitMaskBoxImageOutset", "webkitMaskBoxImageRepeat", "webkitMaskBoxImageSlice", "webkitMaskBoxImageSource", "webkitMaskBoxImageWidth", "webkitMaskClip", "webkitMaskComposite", "webkitMaskImage", "webkitMaskOrigin", "webkitMaskPosition", "webkitMaskPositionX", "webkitMaskPositionY", "webkitMaskRepeat", "webkitMaskSize", "webkitMaxLogicalHeight", "webkitMaxLogicalWidth", "webkitMinLogicalHeight", "webkitMinLogicalWidth", "webkitOpacity", "webkitOrder", "webkitPaddingAfter", "webkitPaddingBefore", "webkitPaddingEnd", "webkitPaddingStart", "webkitPerspective", "webkitPerspectiveOrigin", "webkitPerspectiveOriginX", "webkitPerspectiveOriginY", "webkitPrintColorAdjust", "webkitRtlOrdering", "webkitRubyPosition", "webkitShapeImageThreshold", "webkitShapeMargin", "webkitShapeOutside", "webkitTapHighlightColor", "webkitTextCombine", "webkitTextDecorationsInEffect", "webkitTextEmphasis", "webkitTextEmphasisColor", "webkitTextEmphasisPosition", "webkitTextEmphasisStyle", "webkitTextFillColor", "webkitTextOrientation", "webkitTextSecurity", "webkitTextSizeAdjust", "webkitTextStroke", "webkitTextStrokeColor", "webkitTextStrokeWidth", "webkitTransform", "webkitTransformOrigin", "webkitTransformOriginX", "webkitTransformOriginY", "webkitTransformOriginZ", "webkitTransformStyle", "webkitTransition", "webkitTransitionDelay", "webkitTransitionDuration", "webkitTransitionProperty", "webkitTransitionTimingFunction", "webkitUserDrag", "webkitUserModify", "webkitUserSelect", "webkitWritingMode", "whiteSpace", "whiteSpaceCollapse", "widows", "width", "willChange", "wordBreak", "wordSpacing", "wordWrap", "writingMode", "x", "y", "zIndex", "zoom"]
|
|
|
|
allStyleProps.forEach(prop => {
|
|
if (prop === "translate") return;
|
|
|
|
const type = cssValueType(prop);
|
|
|
|
switch (type) {
|
|
case "unit-number":
|
|
HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") {
|
|
if(value === "auto") {
|
|
this.style[prop] = value
|
|
return this
|
|
}
|
|
this.style[prop] = value + unit;
|
|
if (value !== "" && this.style[prop] === "") {
|
|
throw new Error(`Invalid CSS value for ${prop}: ` + value + unit);
|
|
}
|
|
return this;
|
|
});
|
|
break;
|
|
|
|
case "string":
|
|
HTMLElement.prototype[prop] = StyleFunction(function(value) {
|
|
this.style[prop] = value;
|
|
if (value !== "" && this.style[prop] === "") {
|
|
throw new Error(`Invalid CSS value for ${prop}: ` + value);
|
|
}
|
|
return this;
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
extendHTMLElementWithStyleSetters();
|
|
|
|
HTMLElement.prototype.addStateWatcher = function(field, cb) {
|
|
let parent = this
|
|
while(!(parent instanceof Shadow)) {
|
|
parent = parent.parentNode
|
|
}
|
|
parent.stateWatchers[field].push(cb)
|
|
}
|
|
|
|
// Currently only works for one state variable in the function
|
|
// Could probably be fixed by just making lastState an array and clearing it out every function call?
|
|
HTMLElement.prototype.setUpState = function(styleFunc, cb) {
|
|
let format = (value) => {return Array.isArray(value) ? value : [value]}
|
|
|
|
// 1. Run the callback to get the style argument and also update lastState
|
|
let styleArgs = format(cb())
|
|
|
|
// 2. Check if lastState has really been updated. If not, the user-provided cb did not access valid state
|
|
if(!quill.lastState) {
|
|
throw new Error("Quill: style state function does not access valid state")
|
|
}
|
|
|
|
// 3. Construct function to run when state changes
|
|
let onStateChange = () => {
|
|
styleFunc.call(this, ...format(cb()))
|
|
}
|
|
|
|
// 4. Now listen for the state to change
|
|
this.addStateWatcher(quill.lastState, onStateChange)
|
|
|
|
// 5. Run the original function again, this time with the actual arguments
|
|
quill.lastState = null
|
|
styleFunc.call(this, ...styleArgs)
|
|
}
|
|
|
|
function StyleFunction(func) {
|
|
let styleFunction = function(value, unit = "px") {
|
|
if(typeof value === 'function') {
|
|
this.setUpState(styleFunction, value)
|
|
return this
|
|
} else {
|
|
func.call(this, value, unit); // ".call" ensures that "this" is correct
|
|
return this
|
|
}
|
|
}
|
|
|
|
return styleFunction
|
|
}
|
|
|
|
HTMLElement.prototype.styles = function(cb) {
|
|
cb.call(this, this)
|
|
return this
|
|
}
|
|
|
|
/* Type 1 */
|
|
|
|
HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") {
|
|
this.style.paddingTop = value + unit
|
|
this.style.paddingBottom = value + unit
|
|
return this
|
|
})
|
|
|
|
HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") {
|
|
this.style.paddingRight = value + unit
|
|
this.style.paddingLeft = value + unit
|
|
return this
|
|
})
|
|
|
|
HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") {
|
|
this.style.marginTop = value + unit
|
|
this.style.marginBottom = value + unit
|
|
return this
|
|
})
|
|
|
|
HTMLElement.prototype.marginHorizontal = StyleFunction(function(value, unit = "px") {
|
|
this.style.marginRight = value + unit
|
|
this.style.marginLeft = value + unit
|
|
return this
|
|
})
|
|
|
|
HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
|
|
|
|
switch(value) {
|
|
case "6xl":
|
|
value = "3.75"; unit = "rem"
|
|
break;
|
|
|
|
case "5xl":
|
|
value = "3"; unit = "rem"
|
|
break;
|
|
|
|
case "4xl":
|
|
value = "2.25"; unit = "rem"
|
|
break;
|
|
|
|
case "3xl":
|
|
value = "1.875"; unit = "rem"
|
|
break;
|
|
|
|
case "2xl":
|
|
value = "1.5"; unit = "rem"
|
|
break;
|
|
|
|
case "xl":
|
|
value = "1.25"; unit = "rem"
|
|
break;
|
|
|
|
case "l":
|
|
value = "1.125"; unit = "rem"
|
|
break;
|
|
|
|
case "s":
|
|
value = "0.875"; unit = "rem"
|
|
break;
|
|
|
|
case "xs":
|
|
value = "0.75"; unit = "rem"
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
this.style.fontSize = value + unit
|
|
return this
|
|
})
|
|
|
|
|
|
HTMLElement.prototype.width = function(value, unit = "px") {
|
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
this.style.width = value + unit
|
|
if(window.getComputedStyle(this).display === "inline") {
|
|
this.style.display = "block"
|
|
}
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.height = function(value, unit = "px") {
|
|
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
this.style.height = value + unit
|
|
if(window.getComputedStyle(this).display === "inline") {
|
|
this.style.display = "block"
|
|
}
|
|
return this
|
|
}
|
|
|
|
function checkPositionType(el) {
|
|
let computed = window.getComputedStyle(el).position
|
|
if(!(computed === "absolute" || computed === "fixed")) {
|
|
el.style.position = "absolute"
|
|
}
|
|
}
|
|
|
|
HTMLElement.prototype.x = function(value, unit = "px") {
|
|
if (typeof value !== 'number' || isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
checkPositionType(this)
|
|
this.style.left = value + unit
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.y = function(value, unit = "px") {
|
|
if (typeof value !== 'number' || isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
checkPositionType(this)
|
|
this.style.top = value + unit
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.xRight = function(value, unit = "px") {
|
|
if (typeof value !== 'number' || isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
checkPositionType(this)
|
|
this.style.right = value + unit
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.yBottom = function(value, unit = "px") {
|
|
if (typeof value !== 'number' || isNaN(value))
|
|
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
|
checkPositionType(this)
|
|
this.style.bottom = value + unit
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.backgroundImage = function (...values) {
|
|
const formatted = values
|
|
.map(v => {
|
|
if(v.includes("/") && !v.includes("gradient")) {
|
|
v = "url(" + v + ")"
|
|
}
|
|
return String(v).trim();
|
|
})
|
|
.join(", ");
|
|
|
|
this.style.backgroundImage = formatted;
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.center = function () {
|
|
this.style.transform = "translate(-50%, -50%)"
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.centerX = function () {
|
|
this.style.transform = "translateX(-50%)"
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.centerY = function () {
|
|
this.style.transform = "translateY(-50%)"
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.alignVertical = function (value) {
|
|
const direction = getComputedStyle(this).flexDirection;
|
|
if(!direction) {
|
|
throw new Error("alignVertical can be only be used on HStacks or VStacks!")
|
|
}
|
|
|
|
if (direction === "column" || direction === "column-reverse") {
|
|
this.style.justifyContent = value;
|
|
} else {
|
|
this.style.alignItems = value;
|
|
}
|
|
return this
|
|
}
|
|
|
|
HTMLElement.prototype.alignHorizontal = function (value) {
|
|
const direction = getComputedStyle(this).flexDirection;
|
|
if(!direction) {
|
|
throw new Error("alignHorizontal can be only be used on HStacks or VStacks!")
|
|
}
|
|
|
|
if (direction === "column" || direction === "column-reverse") {
|
|
this.style.alignItems = value;
|
|
} else {
|
|
this.style.justifyContent = value;
|
|
}
|
|
return this
|
|
}
|
|
|
|
|
|
/* Elements */
|
|
|
|
quill.setChildren = function(el, innerContent) {
|
|
if(typeof innerContent === "string") {
|
|
el.innerText = innerContent
|
|
} else if(typeof innerContent === "function") {
|
|
el.render = innerContent
|
|
} else {
|
|
throw new Error("Children of unknown type")
|
|
}
|
|
}
|
|
|
|
window.a = function a( href, inner=href ) {
|
|
if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
|
|
let link = document.createElement("a")
|
|
link.setAttribute('href', href);
|
|
quill.setChildren(link, inner)
|
|
quill.render(link)
|
|
return link
|
|
}
|
|
|
|
window.img = function img(src, width="", height="") {
|
|
let image = document.createElement("img")
|
|
|
|
if(!src || !(typeof src==="string")) {
|
|
throw new Error("img: missing first argument: src | String")
|
|
} else {
|
|
image.src = src
|
|
}
|
|
if(width && typeof width === "string") {
|
|
image.style.width = width
|
|
} else if(width) {
|
|
image.style.width = width + "px"
|
|
}
|
|
if(height && typeof height === "string") {
|
|
image.style.height = height
|
|
} else if(height) {
|
|
image.style.height = height + "px"
|
|
}
|
|
quill.render(image)
|
|
return image
|
|
}
|
|
|
|
HTMLImageElement.prototype.backgroundColor = function(value) {
|
|
if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
|
|
fetch(this.src).then(response => response.text())
|
|
.then(svgText => {
|
|
const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
|
|
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
|
|
this.src = URL.createObjectURL(blob);
|
|
}).catch(error => {
|
|
console.error('Error updating SVG fill:', error);
|
|
});
|
|
} else {
|
|
this.style.backgroundColor = value;
|
|
}
|
|
|
|
return this; // Always returns the element itself
|
|
};
|
|
|
|
window.p = function p(innerHTML) {
|
|
let el = document.createElement("p")
|
|
if(typeof innerText === "function") {
|
|
el.render = innerHTML
|
|
} else {
|
|
el.innerHTML = innerHTML
|
|
}
|
|
el.style.margin = "0";
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.h1 = function h1(innerText) {
|
|
let el = document.createElement("h1")
|
|
el.innerText = innerText
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.h2 = function h2(innerText) {
|
|
let el = document.createElement("h2")
|
|
el.innerText = innerText
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.h3 = function h3(innerText) {
|
|
let el = document.createElement("h3")
|
|
el.innerText = innerText
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.div = function (innerText) {
|
|
let el = document.createElement("div")
|
|
el.innerText = innerText ?? ""
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.span = function (innerText) {
|
|
let el = document.createElement("span")
|
|
el.innerText = innerText
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.button = function (children = "") {
|
|
let el = document.createElement("button")
|
|
quill.setChildren(el, children)
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.form = function(cb) {
|
|
let el = document.createElement("form")
|
|
el.render = cb
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.input = function(placeholder = "", width, height) {
|
|
let el = document.createElement("input")
|
|
el.placeholder = placeholder
|
|
el.style.width = width
|
|
el.style.height = height
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.label = function(inside) {
|
|
let el = document.createElement("label")
|
|
if(typeof inside === "string") {
|
|
el.innerText = inside
|
|
} else {
|
|
el.render = inside
|
|
}
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
window.textarea = function(placeholder = "") {
|
|
let el = document.createElement("textarea")
|
|
el.placeholder = placeholder
|
|
quill.render(el)
|
|
return el
|
|
}
|
|
|
|
|
|
/* STACKS */
|
|
|
|
handleStack = function(cb, name, styles="") {
|
|
let nowRendering = quill.rendering[quill.rendering.length-1]
|
|
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
|
nowRendering.style.cssText += styles
|
|
nowRendering.classList.add(name)
|
|
quill.lastState = null
|
|
cb()
|
|
if(quill.lastState) {
|
|
nowRendering.addStateWatcher(quill.lastState, quill.rerenderStackContents.bind(nowRendering, nowRendering, cb))
|
|
}
|
|
return nowRendering
|
|
} else {
|
|
let div = document.createElement("div")
|
|
div.classList.add(name)
|
|
div.style.cssText += styles
|
|
div.render = cb
|
|
quill.render(div)
|
|
return div
|
|
}
|
|
}
|
|
|
|
window.VStack = function (cb = () => {}) {
|
|
let styles = `
|
|
display: flex;
|
|
flex-direction: column;
|
|
`
|
|
return handleStack(cb, "VStack", styles)
|
|
}
|
|
|
|
window.HStack = function (cb = () => {}) {
|
|
let styles = `
|
|
display: flex;
|
|
flex-direction: row;
|
|
`;
|
|
return handleStack(cb, "HStack", styles)
|
|
};
|
|
|
|
window.ZStack = function (cb = () => {}) {
|
|
return handleStack(cb, "ZStack")
|
|
};
|
|
|
|
/* SHAPES */
|
|
|
|
window.svgMethods = function(svg) {
|
|
svg.pulse = function (duration = 600) {
|
|
this.style.transition = `transform ${duration}ms ease-in-out`
|
|
this.style.transform = "scale(1.2)"
|
|
setTimeout(() => {
|
|
this.style.transform = "scale(1)"
|
|
}, duration / 2)
|
|
return this
|
|
}
|
|
|
|
// Rotate (e.g. loading spinner)
|
|
svg.rotate = function (degrees = 360, duration = 1000) {
|
|
this.style.transition = `transform ${duration}ms linear`
|
|
this.style.transform = `rotate(${degrees}deg)`
|
|
return this
|
|
}
|
|
|
|
// Change color
|
|
svg.fill = function (color) {
|
|
this.setAttribute("fill", color)
|
|
return this
|
|
}
|
|
|
|
svg.height = function (height) {
|
|
this.setAttribute("height", height)
|
|
return this
|
|
}
|
|
|
|
svg.width = function (width) {
|
|
this.setAttribute("width", width)
|
|
return this
|
|
}
|
|
|
|
svg.stroke = function (width, color) {
|
|
this.setAttribute("stroke", color)
|
|
this.setAttribute("stroke-width", width)
|
|
return this
|
|
}
|
|
|
|
// Toggle visibility
|
|
svg.toggle = function () {
|
|
this.style.display = this.style.display === "none" ? "" : "none"
|
|
return this
|
|
}
|
|
}
|
|
|
|
window.Rectangle = function (width = "40px", height = "40px") {
|
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
const svgEl = document.createElementNS(svgNS, "svg");
|
|
const rectEl = document.createElementNS(svgNS, "rect");
|
|
|
|
// SVG size
|
|
svgEl.setAttribute("width", width);
|
|
svgEl.setAttribute("height", height);
|
|
svgEl.setAttribute("viewBox", "0 0 100 100");
|
|
svgEl.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
|
|
// Rectangle: full size, slightly rounded corners
|
|
rectEl.setAttribute("x", "15"); // 15% margin from edges
|
|
rectEl.setAttribute("y", "15");
|
|
rectEl.setAttribute("width", "70"); // 70% of viewBox
|
|
rectEl.setAttribute("height", "70");
|
|
// rectEl.setAttribute("rx", "8"); // rounded corners (optional)
|
|
// rectEl.setAttribute("ry", "8");
|
|
|
|
svgEl.appendChild(rectEl);
|
|
svgMethods(svgEl); // assuming you have this
|
|
quill.render(svgEl);
|
|
return svgEl;
|
|
}
|
|
|
|
window.Triangle = function (width = "40px", height = "40px") {
|
|
const svgNS = "http://www.w3.org/2000/svg"
|
|
const svgEl = document.createElementNS(svgNS, "svg")
|
|
const pathEl = document.createElementNS(svgNS, "path")
|
|
|
|
// SVG size
|
|
svgEl.setAttribute("width", width)
|
|
svgEl.setAttribute("height", height)
|
|
svgEl.setAttribute("viewBox", "0 0 100 100")
|
|
svgEl.setAttribute("preserveAspectRatio", "xMidYMid meet")
|
|
// Right-pointing triangle (Play icon)
|
|
pathEl.setAttribute("d", "M 25 15 L 90 50 L 25 85 Z") // ◄ adjust points if needed
|
|
|
|
svgEl.appendChild(pathEl)
|
|
svgMethods(svgEl)
|
|
quill.render(svgEl)
|
|
return svgEl
|
|
}
|
|
|
|
|
|
/* EVENTS */
|
|
|
|
HTMLElement.prototype.onAppear = function(func) {
|
|
func.call(this);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onClick = function(func) {
|
|
const onMouseDown = (e) => func.call(this, false, e);
|
|
const onMouseUp = (e) => func.call(this, true, e);
|
|
this._storeListener("mousedown", onMouseDown);
|
|
this._storeListener("mouseup", onMouseUp);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onMouseDown = function(func) {
|
|
this._storeListener("mousedown", func);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onMouseUp = function(func) {
|
|
this._storeListener("mouseup", func);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onRightClick = function(func) {
|
|
this._storeListener("contextmenu", func);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onHover = function(cb) {
|
|
const onEnter = (e) => cb.call(this, true, e);
|
|
const onLeave = (e) => cb.call(this, false, e);
|
|
this._storeListener("mouseover", onEnter);
|
|
this._storeListener("mouseleave", onLeave);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onFocus = function(cb) {
|
|
if (!this.matches('input, textarea, select, button')) {
|
|
throw new Error("Can't put focus event on non-form element!");
|
|
}
|
|
const onFocus = () => cb.call(this, true);
|
|
const onBlur = () => cb.call(this, false);
|
|
this._storeListener("focus", onFocus);
|
|
this._storeListener("blur", onBlur);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onKeyDown = function(cb) {
|
|
this._storeListener("keydown", cb);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onInput = function(cb) {
|
|
if(!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]'))
|
|
throw new Error("Can't put input event on non-input element!")
|
|
this._storeListener("input", cb);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onChange = function(cb) {
|
|
if(!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]'))
|
|
throw new Error("Can't put input event on non-input element!")
|
|
this._storeListener("change", cb);
|
|
return this;
|
|
};
|
|
|
|
|
|
HTMLElement.prototype.onSubmit = function(cb) {
|
|
if(!this.matches('form'))
|
|
throw new Error("Can't put form event on non-form element!")
|
|
this._storeListener("submit", cb);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onTouch = function(cb) {
|
|
const onStart = () => cb.call(this, true);
|
|
const onEnd = () => cb.call(this, false);
|
|
const onCancel = () => cb.call(this, null);
|
|
this._storeListener("touchstart", onStart);
|
|
this._storeListener("touchend", onEnd);
|
|
this._storeListener("touchcancel", onCancel);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype.onTap = function(cb) {
|
|
this._storeListener("touchend", cb);
|
|
return this;
|
|
};
|
|
|
|
/* WHY THIS LISTENER IS THE WAY IT IS:
|
|
- We can't just put a listener on the element, because a window "navigate" event won't trigger it
|
|
- We can't just put a listener on the window, because the "this" variable will only refer to the window
|
|
- And, if we try to re-add that scope using bind(), it makes the return value of .toString() unreadable, which means we cannot detect duplicate listeners.
|
|
- Therefore, we attach a global navigate event to the window, and each navigate event in this array, and manually trigger each event when the global one fires.
|
|
*/
|
|
navigateListeners = []
|
|
window.addEventListener("navigate", () => {
|
|
for(entry of navigateListeners) {
|
|
entry.el.dispatchEvent(new CustomEvent("navigate"))
|
|
}
|
|
})
|
|
HTMLElement.prototype.onNavigate = function(cb) {
|
|
this._storeListener("navigate", cb);
|
|
|
|
let found = false
|
|
let elementIndex = Array.from(this.parentNode.children).indexOf(this)
|
|
for(entry of navigateListeners) {
|
|
if(
|
|
entry.cb.toString() === cb.toString()
|
|
&& entry.index === elementIndex
|
|
&& this.nodeName === entry.el.nodeName
|
|
) {
|
|
found = true
|
|
break;
|
|
}
|
|
}
|
|
if(found === false) {
|
|
navigateListeners.push({el: this, cb: cb, index: elementIndex})
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/*
|
|
Same principle applies
|
|
*/
|
|
queryListeners = []
|
|
HTMLElement.prototype.onQueryChanged = function(cb) {
|
|
this._storeListener("query-changed", cb);
|
|
|
|
let found = false
|
|
for(entry of queryListeners) {
|
|
if(entry.cb.toString() === cb.toString() &&
|
|
this.nodeName === entry.el.nodeName) {
|
|
found = true
|
|
break;
|
|
}
|
|
}
|
|
if(found === false) {
|
|
queryListeners.push({el: this, cb: cb})
|
|
}
|
|
|
|
return this;
|
|
};
|
|
window.addEventListener("query-changed", () => {
|
|
for(entry of queryListeners) {
|
|
entry.el.dispatchEvent(new CustomEvent("query-changed"))
|
|
}
|
|
})
|
|
|
|
HTMLElement.prototype.onEvent = function(name, cb) {
|
|
window._storeListener(window, name, cb);
|
|
return this;
|
|
};
|
|
|
|
HTMLElement.prototype._storeListener = function(type, handler, options) {
|
|
window._storeListener(this, type, handler, options)
|
|
}
|
|
|
|
window.__listeners = []
|
|
|
|
function _storeListener(target, type, handler, options) {
|
|
if (!target.__listeners) target.__listeners = [];
|
|
|
|
const optionsString = JSON.stringify(options);
|
|
|
|
const index = target.__listeners.findIndex(listener =>
|
|
listener.type === type &&
|
|
listener.handler.toString() === handler.toString() &&
|
|
JSON.stringify(listener.options) === optionsString
|
|
);
|
|
|
|
if (index === -1) { // Listener is new
|
|
target.addEventListener(type, handler, options);
|
|
target.__listeners.push({ type, handler, options });
|
|
} else { // Listener is a duplicate, can be replaced
|
|
const old = target.__listeners[index];
|
|
target.removeEventListener(old.type, old.handler, old.options);
|
|
|
|
// Replace with the new one
|
|
target.addEventListener(type, handler, options);
|
|
target.__listeners[index] = { type, handler, options };
|
|
}
|
|
}
|
|
|
|
HTMLElement.prototype.removeAllListeners = function() {
|
|
if (!this.__listeners) return;
|
|
for (const { type, handler, options } of this.__listeners) {
|
|
this.removeEventListener(type, handler, options);
|
|
}
|
|
this.__listeners = [];
|
|
return this;
|
|
};
|
|
|
|
/* ATTRIBUTES */
|
|
|
|
HTMLElement.prototype.attr = function(attributes) {
|
|
if (
|
|
typeof attributes !== "object" ||
|
|
attributes === null ||
|
|
Array.isArray(attributes)
|
|
) {
|
|
throw new TypeError("attr() expects an object with key-value pairs");
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(attributes)) {
|
|
this.setAttribute(key, value);
|
|
}
|
|
return this;
|
|
};
|