diff --git a/src/_/code/quill.js b/src/_/code/quill.js index d44d0fd..19242eb 100644 --- a/src/_/code/quill.js +++ b/src/_/code/quill.js @@ -1,6 +1,18 @@ /* Sam Russell Captured Sun + 1.5.26 - Switching verticalAlign and horizontalAlign names, adding borderVertical and Horizontal + 12.26.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 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() @@ -22,6 +34,23 @@ history.pushState = function pushState() { 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')); @@ -43,6 +72,12 @@ window.$ = function(selector, el = document) { 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 */ @@ -138,6 +173,7 @@ Object.defineProperty(Array.prototype, 'last', { window.quill = { rendering: [], + lastState: null, render: (el) => { if(el instanceof Shadow) { @@ -168,19 +204,6 @@ window.quill = { quill.rendering.pop() }, - loadPage: () => { - let URL = window.location.pathname - if(!window.routes[URL]) { - throw new Error("No URL for this route: ", URL) - } - - let pageClass = window[routes[URL]] - document.title = pageClass.title ?? document.title - document.body.innerHTML = "" - let page = new pageClass() - quill.render(page) - }, - isStack: (el) => { return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack") }, @@ -191,6 +214,7 @@ window.Shadow = class extends HTMLElement { super() } } + window.register = (el, tagname) => { if (typeof el.prototype.render !== 'function') { throw new Error("Element must have a render: " + el.prototype.constructor.name) @@ -205,6 +229,75 @@ window.register = (el, tagname) => { 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 } @@ -216,6 +309,9 @@ HTMLElement.prototype.rerender = function() { /* Styling */ +window.pct = "%" +window.vmin = "vmin" +window.vmax = "vmax" window.vh = "vh" window.vw = "vw" window.px = "px" @@ -256,53 +352,77 @@ function extendHTMLElementWithStyleSetters() { const div = document.createElement("div"); const style = div.style; if (!(prop in style)) return "invalid"; - - // Does prop accept a value with px? - style[prop] = "10px"; - if (style[prop] === "10px") return "unit-number"; - - // Does prop accept a unitless number (opacity, zIndex, etc)? - style[prop] = "5"; - const computed = getComputedStyle(div)[prop]; - if (!isNaN(parseFloat(computed))) return "unitless-number"; - - return "string"; + + 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; - if(prop === "position") console.log("position") const type = cssValueType(prop); switch (type) { case "unit-number": - HTMLElement.prototype[prop] = function(value, unit = "px") { - if (typeof value !== "number" || isNaN(value)) { - throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`); + HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") { + if(value === "auto") { + this.style[prop] = value + return this } this.style[prop] = value + unit; - return this; - }; - break; - - case "unitless-number": - HTMLElement.prototype[prop] = function(value) { - if (typeof value !== "number" || isNaN(value)) { - throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`); + if (value !== "" && this.style[prop] === "") { + throw new Error(`Invalid CSS value for ${prop}: ` + value + unit); } - this.style[prop] = String(value); return this; - }; + }); break; case "string": - HTMLElement.prototype[prop] = function(value) { + 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; } }); @@ -310,75 +430,98 @@ function extendHTMLElementWithStyleSetters() { extendHTMLElementWithStyleSetters(); -HTMLElement.prototype.padding = function(one, two, three = "px") { +HTMLElement.prototype.addStateWatcher = function(field, cb) { + let parent = this + while(!(parent instanceof Shadow)) { + parent = parent.parentNode + } + parent.stateWatchers[field].push(cb) +} - const setPadding = (side, val) => { - const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`; - this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val; - }; +// 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]} - if(one === "horizontal" || one === "vertical") { // is one a direction - if (one === "horizontal") { - setPadding("left", two); - setPadding("right", two); - } else if (one === "vertical") { - setPadding("top", two); - setPadding("bottom", two); - } - } else { // is two a value - if(typeof one !== 'number' || isNaN(one)) { - this.style.padding = one + // 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 { - this.style.padding = one + two + func.call(this, value, unit); // ".call" ensures that "this" is correct + return this } } - - return this; -}; - HTMLElement.prototype.paddingVertical = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); - this.style.paddingTop = value + unit - this.style.paddingBottom = value + unit - return this - } + return styleFunction +} - HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); - this.style.paddingRight = value + unit - this.style.paddingLeft = value + unit - return this - } +HTMLElement.prototype.styles = function(cb) { + cb.call(this, this) + return this +} -HTMLElement.prototype.margin = function(direction, value, unit = "px") { - if (!value) { - this.style.margin = direction; - return this; - } +/* Type 1 */ - const setMargin = (side, val) => { - const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`; - this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val; - }; +HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") { + this.style.paddingTop = value + unit + this.style.paddingBottom = value + unit + return this +}) - if (direction === "horizontal") { - setMargin("left", value); - setMargin("right", value); - } else if (direction === "vertical") { - setMargin("top", value); - setMargin("bottom", value); - } else { - setMargin(direction, value); - } +HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") { + this.style.paddingRight = value + unit + this.style.paddingLeft = value + unit + return this +}) - return this; -}; +HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") { + this.style.marginTop = value + unit + this.style.marginBottom = value + unit + return this +}) -HTMLElement.prototype.fontSize = function(value, unit = "px") { - if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value)) - throw new Error(`Invalid value: ${value}. Expected a number.`); +HTMLElement.prototype.marginHorizontal = StyleFunction(function(value, unit = "px") { + this.style.marginRight = value + unit + this.style.marginLeft = value + unit + return this +}) + +HTMLElement.prototype.borderVertical = StyleFunction(function(value) { + this.style.borderTop = value + this.style.borderBottom = value + return this +}) + +HTMLElement.prototype.borderHorizontal = StyleFunction(function(value) { + this.style.borderRight = value + this.style.borderLeft = value + return this +}) + +HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") { switch(value) { case "6xl": @@ -422,6 +565,27 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") { } 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) { @@ -492,6 +656,35 @@ HTMLElement.prototype.centerY = function () { return this; }; +HTMLElement.prototype.verticalAlign = function (value) { + const direction = getComputedStyle(this).flexDirection; + if(!direction) { + throw new Error("verticalAlign 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.horizontalAlign = function (value) { + const direction = getComputedStyle(this).flexDirection; + if(!direction) { + throw new Error("horizontalAlign 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) { @@ -613,7 +806,7 @@ window.form = function(cb) { return el } -window.input = function(placeholder, width, height) { +window.input = function(placeholder = "", width, height) { let el = document.createElement("input") el.placeholder = placeholder el.style.width = width @@ -622,14 +815,18 @@ window.input = function(placeholder, width, height) { return el } -window.label = function(text) { +window.label = function(inside) { let el = document.createElement("label") - el.innerText = text + if(typeof inside === "string") { + el.innerText = inside + } else { + el.render = inside + } quill.render(el) return el } -window.textarea = function(placeholder) { +window.textarea = function(placeholder = "") { let el = document.createElement("textarea") el.placeholder = placeholder quill.render(el) @@ -639,61 +836,47 @@ window.textarea = function(placeholder) { /* STACKS */ -window.VStack = function (cb = () => {}) { - let styles = ` - display: flex; - flex-direction: column; - ` +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("VStack") + nowRendering.classList.add(name) cb() + if(quill.lastState) { + nowRendering.addStateWatcher(quill.lastState, () => { + nowRendering.innerHTML = "" + 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 } +} - let div = document.createElement("div") - div.classList.add("VStack") - 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; + display: flex; + flex-direction: row; `; - let nowRendering = quill.rendering[quill.rendering.length - 1]; - if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) { - nowRendering.style.cssText += styles; - nowRendering.classList.add("HStack") - cb(); - return nowRendering; - } - - let div = document.createElement("div"); - div.classList.add("HStack"); - div.style.cssText += styles; - div.render = cb; - quill.render(div); - return div; + return handleStack(cb, "HStack", styles) }; window.ZStack = function (cb = () => {}) { - let nowRendering = quill.rendering[quill.rendering.length - 1]; - if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) { - nowRendering.classList.add("ZStack") - cb(); - return nowRendering; - } - - let div = document.createElement("div"); - div.classList.add("ZStack"); - div.render = cb; - quill.render(div); - return div; + return handleStack(cb, "ZStack") }; /* SHAPES */ @@ -792,13 +975,13 @@ window.Triangle = function (width = "40px", height = "40px") { /* EVENTS */ HTMLElement.prototype.onAppear = function(func) { - func(this); + func.call(this); return this; }; HTMLElement.prototype.onClick = function(func) { - const onMouseDown = () => func.call(this, true); - const onMouseUp = () => func.call(this, false); + 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; @@ -820,8 +1003,8 @@ HTMLElement.prototype.onRightClick = function(func) { }; HTMLElement.prototype.onHover = function(cb) { - const onEnter = () => cb.call(this, true); - const onLeave = () => cb.call(this, false); + 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; @@ -850,6 +1033,21 @@ HTMLElement.prototype.onInput = function(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); @@ -866,17 +1064,66 @@ HTMLElement.prototype.onTap = function(cb) { }; /* WHY THIS LISTENER IS THE WAY IT IS: -- If we dispatch the "navigate" event on the window (as one would expect for a "navigate" event), a listener placed on the element will not pick it up. -- However, if we add the listener here on the window, it won't have the "this" scope that a callback normally would. Which makes it much less useful. -- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we cannot detect duplicate listeners. -- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires. +- 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); - window.addEventListener("navigate", () => this.dispatchEvent(new CustomEvent("navigate"))) + + 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; @@ -936,4 +1183,4 @@ HTMLElement.prototype.attr = function(attributes) { this.setAttribute(key, value); } return this; -}; \ No newline at end of file +}; diff --git a/src/js/Home.js b/src/js/Home.js index eadafc2..2a6c5df 100644 --- a/src/js/Home.js +++ b/src/js/Home.js @@ -118,7 +118,8 @@ class Home extends Shadow { VStack(() => { this.logs.map(log => div(log) - .padding("8px 12px") + .paddingHorizontal("8px") + .paddingVertical("12px") .backgroundColor("rgba(255,255,255,0.9)") .borderRadius(6, px) .marginBottom(6, px) @@ -130,7 +131,7 @@ class Home extends Shadow { .attr({"id": "logList"}) .flex(1) .overflowY("auto") - .padding("0 16px") + .paddingHorizontal(16, px) }) .marginTop(7, em) }) @@ -156,26 +157,26 @@ class Home extends Shadow { formatCentralTime(date) { return new Intl.DateTimeFormat('en-US', { - timeZone: 'America/Chicago', - month: '2-digit', - day: '2-digit', - year: '2-digit', - hour: 'numeric', - minute: '2-digit', - second: '2-digit', - hour12: true - }) - .format(date) - .replace(/,/, '') // "04/05/25, 2:30:00 PM" → "04/05/25 2:30:00 PM" - .replace(/\//g, '.') // → "04.05.25 2:30:00 PM" - .toLowerCase() - .replace(' pm', 'pm') - .replace(' am', 'am'); + timeZone: 'America/Chicago', + month: '2-digit', + day: '2-digit', + year: '2-digit', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: true + }) + .format(date) + .replace(/,/, '') // "04/05/25, 2:30:00 PM" → "04/05/25 2:30:00 PM" + .replace(/\//g, '.') // → "04.05.25 2:30:00 PM" + .toLowerCase() + .replace(' pm', 'pm') + .replace(' am', 'am'); } async sendLocation(lat, lon, timestamp) { try { - const resp = await fetch('http://sam.local:3008/api/location', { + const resp = await fetch('http://localhost:3008/api/location', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({