From 17518c3fccc1aab89d36c1133d22ba955ebbcf5a Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Tue, 25 Nov 2025 14:55:22 +1100 Subject: [PATCH] Add auto complete, theme select, & full screen toggle in editors --- static/js/mithril.js | 1698 +++++++++++++++++++++++++++++++++++ static/js/mithril/editor.js | 149 ++- templates/base.html | 4 +- 3 files changed, 1834 insertions(+), 17 deletions(-) create mode 100644 static/js/mithril.js diff --git a/static/js/mithril.js b/static/js/mithril.js new file mode 100644 index 0000000..063079e --- /dev/null +++ b/static/js/mithril.js @@ -0,0 +1,1698 @@ +;(function() { +"use strict" +function Vnode(tag, key, attrs0, children, text, dom) { + return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, is: undefined, domSize: undefined, state: undefined, events: undefined, instance: undefined} +} +Vnode.normalize = function(node) { + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (node == null || typeof node === "boolean") return null + if (typeof node === "object") return node + return Vnode("#", undefined, undefined, String(node), undefined, undefined) +} +Vnode.normalizeChildren = function(input) { + // Preallocate the array length (initially holey) and fill every index immediately in order. + // Benchmarking shows better performance on V8. + var children = new Array(input.length) + // Count the number of keyed normalized vnodes for consistency check. + // Note: this is a perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // the check within updateNodes(), noticeably so. + var numKeyed = 0 + for (var i = 0; i < input.length; i++) { + children[i] = Vnode.normalize(input[i]) + if (children[i] !== null && children[i].key != null) numKeyed++ + } + if (numKeyed !== 0 && numKeyed !== input.length) { + throw new TypeError(children.includes(null) + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." + : "In fragments, vnodes must either all have keys or none have keys." + ) + } + return children +} +// Note: the processing of variadic parameters is perf-sensitive. +// +// In native ES6, it might be preferable to define hyperscript and fragment +// factories with a final ...args parameter and call hyperscriptVnode(...args), +// since modern engines can optimize spread calls. +// +// However, benchmarks showed this was not faster. As a result, spread is used +// only in the parameter lists of hyperscript and fragment, while an array is +// passed to hyperscriptVnode. +var hyperscriptVnode = function(attrs1, children0) { + if (attrs1 == null || typeof attrs1 === "object" && attrs1.tag == null && !Array.isArray(attrs1)) { + if (children0.length === 1 && Array.isArray(children0[0])) children0 = children0[0] + } else { + children0 = children0.length === 0 && Array.isArray(attrs1) ? attrs1 : [attrs1, ...children0] + attrs1 = undefined + } + return Vnode("", attrs1 && attrs1.key, attrs1, children0) +} +// This exists so I'm only saving it once. +var hasOwn = {}.hasOwnProperty +// This is an attrs object that is used by default when attrs is undefined or null. +var emptyAttrs = {} +// This Map manages the following: +// - Whether an attrs is cached attrs generated by compileSelector(). +// - Whether the cached attrs is "static", i.e., does not contain any form attributes. +// These information will be useful to skip updating attrs in render(). +// +// Since the attrs used as keys in this map are not released from the selectorCache object, +// there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap. +var cachedAttrsIsStaticMap = new Map([[emptyAttrs, true]]) +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g +var selectorCache = Object.create(null) +function isEmpty(object) { + for (var key in object) if (hasOwn.call(object, key)) return false + return true +} +function isFormAttributeKey(key) { + return key === "value" || key === "checked" || key === "selectedIndex" || key === "selected" +} +function compileSelector(selector) { + var match, tag = "div", classes = [], attrs = {}, isStatic = true + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") tag = value + else if (type === "#") attrs.id = value + else if (type === ".") classes.push(value) + else if (match[3][0] === "[") { + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") + if (match[4] === "class") classes.push(attrValue) + else { + attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true + if (isFormAttributeKey(match[4])) isStatic = false + } + } + } + if (classes.length > 0) attrs.className = classes.join(" ") + if (isEmpty(attrs)) attrs = emptyAttrs + else cachedAttrsIsStaticMap.set(attrs, isStatic) + return selectorCache[selector] = {tag: tag, attrs: attrs, is: attrs.is} +} +function execSelector(state, vnode) { + vnode.tag = state.tag + var attrs = vnode.attrs + if (attrs == null) { + vnode.attrs = state.attrs + vnode.is = state.is + return vnode + } + if (hasOwn.call(attrs, "class")) { + if (attrs.class != null) attrs.className = attrs.class + attrs.class = null + } + if (state.attrs !== emptyAttrs) { + var className = attrs.className + attrs = Object.assign({}, state.attrs, attrs) + if (state.attrs.className != null) attrs.className = + className != null + ? String(state.attrs.className) + " " + String(className) + : state.attrs.className + } + // workaround for #2622 (reorder keys in attrs to set "type" first) + // The DOM does things to inputs based on the "type", so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (state.tag === "input" && hasOwn.call(attrs, "type")) { + attrs = Object.assign({type: attrs.type}, attrs) + } + // This reduces the complexity of the evaluation of "is" within the render function. + vnode.is = attrs.is + vnode.attrs = attrs + return vnode +} +function hyperscript(selector, attrs, ...children) { + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { + throw Error("The selector must be either a string or a component."); + } + var vnode = hyperscriptVnode(attrs, children) + if (typeof selector === "string") { + vnode.children = Vnode.normalizeChildren(vnode.children) + if (selector !== "[") return execSelector(selectorCache[selector] || compileSelector(selector), vnode) + } + if (vnode.attrs == null) vnode.attrs = {} + vnode.tag = selector + return vnode +} +hyperscript.trust = function(html) { + if (html == null) html = "" + return Vnode("<", undefined, undefined, html, undefined, undefined) +} +hyperscript.fragment = function(attrs4, ...children1) { + var vnode2 = hyperscriptVnode(attrs4, children1) + if (vnode2.attrs == null) vnode2.attrs = {} + vnode2.tag = "[" + vnode2.children = Vnode.normalizeChildren(vnode2.children) + return vnode2 +} +var delayedRemoval = new WeakMap +function *domFor(vnode4) { + // To avoid unintended mangling of the internal bundler, + // parameter destructuring is not used here. + var dom = vnode4.dom + var domSize0 = vnode4.domSize + var generation0 = delayedRemoval.get(dom) + if (dom != null) do { + var nextSibling = dom.nextSibling + if (delayedRemoval.get(dom) === generation0) { + yield dom + domSize0-- + } + dom = nextSibling + } + while (domSize0) +} +var _14 = function() { + var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + } + var currentRedraw + var currentRender + function getDocument(dom) { + return dom.ownerDocument; + } + function getNameSpace(vnode3) { + return vnode3.attrs && vnode3.attrs.xmlns || nameSpace[vnode3.tag] + } + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode3, original) { + if (vnode3.state !== original) throw new Error("'vnode.state' must not be modified.") + } + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode3) { + var original = vnode3.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode3, original) + } + } + // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when + // inside an iframe. Catch and swallow this error, and heavy-handidly return null. + function activeElement(dom) { + try { + return getDocument(dom).activeElement + } catch (e) { + return null + } + } + //create + function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { + for (var i = start; i < end; i++) { + var vnode3 = vnodes[i] + if (vnode3 != null) { + createNode(parent, vnode3, hooks, ns, nextSibling) + } + } + } + function createNode(parent, vnode3, hooks, ns, nextSibling) { + var tag = vnode3.tag + if (typeof tag === "string") { + vnode3.state = {} + if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) + switch (tag) { + case "#": createText(parent, vnode3, nextSibling); break + case "<": createHTML(parent, vnode3, ns, nextSibling); break + case "[": createFragment(parent, vnode3, hooks, ns, nextSibling); break + default: createElement(parent, vnode3, hooks, ns, nextSibling) + } + } + else createComponent(parent, vnode3, hooks, ns, nextSibling) + } + function createText(parent, vnode3, nextSibling) { + vnode3.dom = getDocument(parent).createTextNode(vnode3.children) + insertDOM(parent, vnode3.dom, nextSibling) + } + var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} + function createHTML(parent, vnode3, ns, nextSibling) { + var match0 = vnode3.children.match(/^\s*?<(\w+)/im) || [] + // not using the proper parent makes the child element(s) vanish. + // var div = document.createElement("div") + // div.innerHTML = "ij" + // console.log(div.innerHTML) + // --> "ij", no in sight. + var temp = getDocument(parent).createElement(possibleParents[match0[1]] || "div") + if (ns === "http://www.w3.org/2000/svg") { + temp.innerHTML = "" + vnode3.children + "" + temp = temp.firstChild + } else { + temp.innerHTML = vnode3.children + } + vnode3.dom = temp.firstChild + vnode3.domSize = temp.childNodes.length + var fragment = getDocument(parent).createDocumentFragment() + var child + while (child = temp.firstChild) { + fragment.appendChild(child) + } + insertDOM(parent, fragment, nextSibling) + } + function createFragment(parent, vnode3, hooks, ns, nextSibling) { + var fragment = getDocument(parent).createDocumentFragment() + if (vnode3.children != null) { + var children2 = vnode3.children + createNodes(fragment, children2, 0, children2.length, hooks, null, ns) + } + vnode3.dom = fragment.firstChild + vnode3.domSize = fragment.childNodes.length + insertDOM(parent, fragment, nextSibling) + } + function createElement(parent, vnode3, hooks, ns, nextSibling) { + var tag = vnode3.tag + var attrs5 = vnode3.attrs + var is = vnode3.is + ns = getNameSpace(vnode3) || ns + var element = ns ? + is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : + is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) + vnode3.dom = element + if (attrs5 != null) { + setAttrs(vnode3, attrs5, ns) + } + insertDOM(parent, element, nextSibling) + if (!maybeSetContentEditable(vnode3)) { + if (vnode3.children != null) { + var children2 = vnode3.children + createNodes(element, children2, 0, children2.length, hooks, null, ns) + if (vnode3.tag === "select" && attrs5 != null) setLateSelectAttrs(vnode3, attrs5) + } + } + } + function initComponent(vnode3, hooks) { + var sentinel + if (typeof vnode3.tag.view === "function") { + vnode3.state = Object.create(vnode3.tag) + sentinel = vnode3.state.view + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + } else { + vnode3.state = void 0 + sentinel = vnode3.tag + if (sentinel.$$reentrantLock$$ != null) return + sentinel.$$reentrantLock$$ = true + vnode3.state = (vnode3.tag.prototype != null && typeof vnode3.tag.prototype.view === "function") ? new vnode3.tag(vnode3) : vnode3.tag(vnode3) + } + initLifecycle(vnode3.state, vnode3, hooks) + if (vnode3.attrs != null) initLifecycle(vnode3.attrs, vnode3, hooks) + vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) + if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") + sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode3, hooks, ns, nextSibling) { + initComponent(vnode3, hooks) + if (vnode3.instance != null) { + createNode(parent, vnode3.instance, hooks, ns, nextSibling) + vnode3.dom = vnode3.instance.dom + vnode3.domSize = vnode3.instance.domSize + } + else { + vnode3.domSize = 0 + } + } + //update + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next DOM node if we're dealing with a + * fragment that is not the last item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. discuss DOM node operations. + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed based on the first non-null node + // of each list. + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + // ## Diffing + // + // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837 + // may be good for context on longest increasing subsequence-based logic for moving nodes. + // + // In order to diff keyed lists, one has to + // + // 1) match nodes in both lists, per key, and update them accordingly + // 2) create the nodes present in the new list, but absent in the old one + // 3) remove the nodes present in the old list, but absent in the new one + // 4) figure out what nodes in 1) to move in order to minimize the DOM operations. + // + // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate + // over the new list and for each new vnode, find the corresponding vnode in the old list using + // the map. + // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new + // and must be created. + // For the removals, we actually remove the nodes that have been updated from the old list. + // The nodes that remain in that list after 1) and 2) have been performed can be safely removed. + // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS) + // algorithm. + // + // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going + // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices + // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would + // match the above lists, for example). + // + // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We + // can update those nodes without moving them, and only call `insertNode` on `4` and `5`. + // + // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually + // the longest increasing subsequence *of old nodes still present in the new list*). + // + // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation + // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`, + // the `LIS` and a temporary one to create the LIS). + // + // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of + // the LIS and can be updated without moving them. + // + // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with + // the exception of the last node if the list is fully reversed). + // + // ## Finding the next sibling. + // + // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. + // When the list is being traversed top-down, at any index, the DOM nodes up to the previous + // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. + // + // In the other scenarios (swaps, upwards traversal, map-based diff), + // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the + // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node + // as the next sibling (cached in the `nextSibling` variable). + // ## DOM node moves + // + // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo). We move + // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` + // variable rather than fetching it using `getNextSibling()`. + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + if (old === vnodes || old == null && vnodes == null) return + else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length) + else { + var isOldKeyed = old[0] != null && old[0].key != null + var isKeyed = vnodes[0] != null && vnodes[0].key != null + var start = 0, oldStart = 0 + if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++ + if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++ + if (isOldKeyed !== isKeyed) { + removeNodes(parent, old, oldStart, old.length) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else if (!isKeyed) { + // Don't index past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(parent, o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + } + if (old.length > commonLength) removeNodes(parent, old, start, old.length) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + } else { + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling + // bottom-up + while (oldEnd >= oldStart && end >= start) { + oe = old[oldEnd] + ve = vnodes[end] + if (oe.key !== ve.key) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + } + // top-down + while (oldEnd >= oldStart && end >= start) { + o = old[oldStart] + v = vnodes[start] + if (o.key !== v.key) break + oldStart++, start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + } + // swaps and list reversals + while (oldEnd >= oldStart && end >= start) { + if (start === end) break + if (o.key !== ve.key || oe.key !== v.key) break + topSibling = getNextSibling(old, oldStart, nextSibling) + moveDOM(parent, oe, topSibling) + if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns) + if (++start <= --end) moveDOM(parent, o, nextSibling) + if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldStart++; oldEnd-- + oe = old[oldEnd] + ve = vnodes[end] + o = old[oldStart] + v = vnodes[start] + } + // bottom up once again + while (oldEnd >= oldStart && end >= start) { + if (oe.key !== ve.key) break + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + oldEnd--, end-- + oe = old[oldEnd] + ve = vnodes[end] + } + if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1) + else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul + var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices + for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1 + for (i = end; i >= start; i--) { + if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) + ve = vnodes[i] + var oldIndex = map[ve.key] + if (oldIndex != null) { + pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered + oldIndices[i-start] = oldIndex + oe = old[oldIndex] + old[oldIndex] = null + if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) + if (ve.dom != null) nextSibling = ve.dom + matched++ + } + } + nextSibling = originalNextSibling + if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1) + if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + else { + if (pos === -1) { + // the indices of the indices of the items that are part of the + // longest increasing subsequence in the oldIndices list + lisIndices = makeLisIndices(oldIndices) + li = lisIndices.length - 1 + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + else { + if (lisIndices[li] === i - start) li-- + else moveDOM(parent, v, nextSibling) + } + if (v.dom != null) nextSibling = vnodes[i].dom + } + } else { + for (i = end; i >= start; i--) { + v = vnodes[i] + if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = vnodes[i].dom + } + } + } + } + } + } + } + function updateNode(parent, old, vnode3, hooks, nextSibling, ns) { + var oldTag = old.tag, tag = vnode3.tag + if (oldTag === tag && old.is === vnode3.is) { + vnode3.state = old.state + vnode3.events = old.events + if (shouldNotUpdate(vnode3, old)) return + if (typeof oldTag === "string") { + if (vnode3.attrs != null) { + updateLifecycle(vnode3.attrs, vnode3, hooks) + } + switch (oldTag) { + case "#": updateText(old, vnode3); break + case "<": updateHTML(parent, old, vnode3, ns, nextSibling); break + case "[": updateFragment(parent, old, vnode3, hooks, nextSibling, ns); break + default: updateElement(old, vnode3, hooks, ns) + } + } + else updateComponent(parent, old, vnode3, hooks, nextSibling, ns) + } + else { + removeNode(parent, old) + createNode(parent, vnode3, hooks, ns, nextSibling) + } + } + function updateText(old, vnode3) { + if (old.children.toString() !== vnode3.children.toString()) { + old.dom.nodeValue = vnode3.children + } + vnode3.dom = old.dom + } + function updateHTML(parent, old, vnode3, ns, nextSibling) { + if (old.children !== vnode3.children) { + removeDOM(parent, old) + createHTML(parent, vnode3, ns, nextSibling) + } + else { + vnode3.dom = old.dom + vnode3.domSize = old.domSize + } + } + function updateFragment(parent, old, vnode3, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode3.children, hooks, nextSibling, ns) + var domSize = 0, children2 = vnode3.children + vnode3.dom = null + if (children2 != null) { + for (var i = 0; i < children2.length; i++) { + var child = children2[i] + if (child != null && child.dom != null) { + if (vnode3.dom == null) vnode3.dom = child.dom + domSize += child.domSize || 1 + } + } + } + vnode3.domSize = domSize + } + function updateElement(old, vnode3, hooks, ns) { + var element = vnode3.dom = old.dom + ns = getNameSpace(vnode3) || ns + if (old.attrs != vnode3.attrs || (vnode3.attrs != null && !cachedAttrsIsStaticMap.get(vnode3.attrs))) { + updateAttrs(vnode3, old.attrs, vnode3.attrs, ns) + } + if (!maybeSetContentEditable(vnode3)) { + updateNodes(element, old.children, vnode3.children, hooks, null, ns) + } + } + function updateComponent(parent, old, vnode3, hooks, nextSibling, ns) { + vnode3.instance = Vnode.normalize(callHook.call(vnode3.state.view, vnode3)) + if (vnode3.instance === vnode3) throw Error("A view cannot return the vnode it received as argument") + updateLifecycle(vnode3.state, vnode3, hooks) + if (vnode3.attrs != null) updateLifecycle(vnode3.attrs, vnode3, hooks) + if (vnode3.instance != null) { + if (old.instance == null) createNode(parent, vnode3.instance, hooks, ns, nextSibling) + else updateNode(parent, old.instance, vnode3.instance, hooks, nextSibling, ns) + vnode3.dom = vnode3.instance.dom + vnode3.domSize = vnode3.instance.domSize + } + else { + if (old.instance != null) removeNode(parent, old.instance) + vnode3.domSize = 0 + } + } + function getKeyMap(vnodes, start, end) { + var map = Object.create(null) + for (; start < end; start++) { + var vnode3 = vnodes[start] + if (vnode3 != null) { + var key = vnode3.key + if (key != null) map[key] = start + } + } + return map + } + // Lifted from ivi https://github.com/ivijs/ivi/ + // takes a list of unique numbers (-1 is special and can + // occur multiple times) and returns an array with the indices + // of the items that are part of the longest increasing + // subsequence + var lisTemp = [] + function makeLisIndices(a) { + var result = [0] + var u = 0, v = 0, i = 0 + var il = lisTemp.length = a.length + for (var i = 0; i < il; i++) lisTemp[i] = a[i] + for (var i = 0; i < il; ++i) { + if (a[i] === -1) continue + var j = result[result.length - 1] + if (a[j] < a[i]) { + lisTemp[i] = j + result.push(i) + continue + } + u = 0 + v = result.length - 1 + while (u < v) { + // Fast integer average without overflow. + // eslint-disable-next-line no-bitwise + var c = (u >>> 1) + (v >>> 1) + (u & v & 1) + if (a[result[c]] < a[i]) { + u = c + 1 + } + else { + v = c + } + } + if (a[i] < a[result[u]]) { + if (u > 0) lisTemp[i] = result[u - 1] + result[u] = i + } + } + u = result.length + v = result[u - 1] + while (u-- > 0) { + result[u] = v + v = lisTemp[v] + } + lisTemp.length = 0 + return result + } + function getNextSibling(vnodes, i, nextSibling) { + for (; i < vnodes.length; i++) { + if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom + } + return nextSibling + } + // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) + function moveDOM(parent, vnode3, nextSibling) { + if (vnode3.dom != null) { + var target + if (vnode3.domSize == null || vnode3.domSize === 1) { + // don't allocate for the common case + target = vnode3.dom + } else { + target = getDocument(parent).createDocumentFragment() + for (var dom of domFor(vnode3)) target.appendChild(dom) + } + insertDOM(parent, target, nextSibling) + } + } + function insertDOM(parent, dom, nextSibling) { + if (nextSibling != null) parent.insertBefore(dom, nextSibling) + else parent.appendChild(dom) + } + function maybeSetContentEditable(vnode3) { + if (vnode3.attrs == null || ( + vnode3.attrs.contenteditable == null && // attribute + vnode3.attrs.contentEditable == null // property + )) return false + var children2 = vnode3.children + if (children2 != null && children2.length === 1 && children2[0].tag === "<") { + var content = children2[0].children + if (vnode3.dom.innerHTML !== content) vnode3.dom.innerHTML = content + } + else if (children2 != null && children2.length !== 0) throw new Error("Child node of a contenteditable must be trusted.") + return true + } + //remove + function removeNodes(parent, vnodes, start, end) { + for (var i = start; i < end; i++) { + var vnode3 = vnodes[i] + if (vnode3 != null) removeNode(parent, vnode3) + } + } + function tryBlockRemove(parent, vnode3, source, counter) { + var original = vnode3.state + var result = callHook.call(source.onbeforeremove, vnode3) + if (result == null) return + var generation = currentRender + for (var dom of domFor(vnode3)) delayedRemoval.set(dom, generation) + counter.v++ + Promise.resolve(result).finally(function () { + checkState(vnode3, original) + tryResumeRemove(parent, vnode3, counter) + }) + } + function tryResumeRemove(parent, vnode3, counter) { + if (--counter.v === 0) { + onremove(vnode3) + removeDOM(parent, vnode3) + } + } + function removeNode(parent, vnode3) { + var counter = {v: 1} + if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeremove === "function") tryBlockRemove(parent, vnode3, vnode3.state, counter) + if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === "function") tryBlockRemove(parent, vnode3, vnode3.attrs, counter) + tryResumeRemove(parent, vnode3, counter) + } + function removeDOM(parent, vnode3) { + if (vnode3.dom == null) return + if (vnode3.domSize == null || vnode3.domSize === 1) { + parent.removeChild(vnode3.dom) + } else { + for (var dom of domFor(vnode3)) parent.removeChild(dom) + } + } + function onremove(vnode3) { + if (typeof vnode3.tag !== "string" && typeof vnode3.state.onremove === "function") callHook.call(vnode3.state.onremove, vnode3) + if (vnode3.attrs && typeof vnode3.attrs.onremove === "function") callHook.call(vnode3.attrs.onremove, vnode3) + if (typeof vnode3.tag !== "string") { + if (vnode3.instance != null) onremove(vnode3.instance) + } else { + if (vnode3.events != null) vnode3.events._ = null + var children2 = vnode3.children + if (Array.isArray(children2)) { + for (var i = 0; i < children2.length; i++) { + var child = children2[i] + if (child != null) onremove(child) + } + } + } + } + //attrs + function setAttrs(vnode3, attrs5, ns) { + for (var key in attrs5) { + setAttr(vnode3, key, null, attrs5[key], ns) + } + } + function setAttr(vnode3, key, old, value, ns) { + if (key === "key" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode3, key)) && typeof value !== "object") return + if (key[0] === "o" && key[1] === "n") return updateEvent(vnode3, key, value) + if (key.slice(0, 6) === "xlink:") vnode3.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) + else if (key === "style") updateStyle(vnode3.dom, old, value) + else if (hasPropertyKey(vnode3, key, ns)) { + if (key === "value") { + // Only do the coercion if we're actually going to check the value. + /* eslint-disable no-implicit-coercion */ + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + //setting input[type=file][value] to same value causes an error to be generated if it's non-empty + //minlength/maxlength validation isn't performed on script-set values(#2256) + if ((vnode3.tag === "input" || vnode3.tag === "textarea") && vnode3.dom.value === "" + value) return + //setting select[value] to same value while having select open blinks select dropdown in Chrome + if (vnode3.tag === "select" && old !== null && vnode3.dom.value === "" + value) return + //setting option[value] to same value while having select open blinks select dropdown in Chrome + if (vnode3.tag === "option" && old !== null && vnode3.dom.value === "" + value) return + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (vnode3.tag === "input" && vnode3.attrs.type === "file" && "" + value !== "") { console.error("`value` is read-only on file inputs!"); return } + /* eslint-enable no-implicit-coercion */ + } + // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. + if (vnode3.tag === "input" && key === "type") vnode3.dom.setAttribute(key, value) + else vnode3.dom[key] = value + } else { + if (typeof value === "boolean") { + if (value) vnode3.dom.setAttribute(key, "") + else vnode3.dom.removeAttribute(key) + } + else vnode3.dom.setAttribute(key === "className" ? "class" : key, value) + } + } + function removeAttr(vnode3, key, old, ns) { + if (key === "key" || old == null || isLifecycleMethod(key)) return + if (key[0] === "o" && key[1] === "n") updateEvent(vnode3, key, undefined) + else if (key === "style") updateStyle(vnode3.dom, old, null) + else if ( + hasPropertyKey(vnode3, key, ns) + && key !== "className" + && key !== "title" // creates "null" as title + && !(key === "value" && ( + vnode3.tag === "option" + || vnode3.tag === "select" && vnode3.dom.selectedIndex === -1 && vnode3.dom === activeElement(vnode3.dom) + )) + && !(vnode3.tag === "input" && key === "type") + ) { + vnode3.dom[key] = null + } else { + var nsLastIndex = key.indexOf(":") + if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1) + if (old !== false) vnode3.dom.removeAttribute(key === "className" ? "class" : key) + } + } + function setLateSelectAttrs(vnode3, attrs5) { + if ("value" in attrs5) { + if(attrs5.value === null) { + if (vnode3.dom.selectedIndex !== -1) vnode3.dom.value = null + } else { + var normalized = "" + attrs5.value // eslint-disable-line no-implicit-coercion + if (vnode3.dom.value !== normalized || vnode3.dom.selectedIndex === -1) { + vnode3.dom.value = normalized + } + } + } + if ("selectedIndex" in attrs5) setAttr(vnode3, "selectedIndex", null, attrs5.selectedIndex, undefined) + } + function updateAttrs(vnode3, old, attrs5, ns) { + // Some attributes may NOT be case-sensitive (e.g. data-***), + // so removal should be done first to prevent accidental removal for newly setting values. + var val + if (old != null) { + if (old === attrs5 && !cachedAttrsIsStaticMap.has(attrs5)) { + console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") + } + for (var key in old) { + if (((val = old[key]) != null) && (attrs5 == null || attrs5[key] == null)) { + removeAttr(vnode3, key, val, ns) + } + } + } + if (attrs5 != null) { + for (var key in attrs5) { + setAttr(vnode3, key, old && old[key], attrs5[key], ns) + } + } + } + function isFormAttribute(vnode3, attr) { + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && (vnode3.dom === activeElement(vnode3.dom) || vnode3.tag === "option" && vnode3.dom.parentNode === activeElement(vnode3.dom)) + } + function isLifecycleMethod(attr) { + return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" + } + function hasPropertyKey(vnode3, key, ns) { + // Filter out namespaced keys + return ns === undefined && ( + // If it's a custom element, just keep it. + vnode3.tag.indexOf("-") > -1 || vnode3.is || + // If it's a normal element, let's try to avoid a few browser bugs. + key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type" + // Defer the property check until *after* we check everything. + ) && key in vnode3.dom + } + //style + function updateStyle(element, old, style) { + if (old === style) { + // Styles are equivalent, do nothing. + } else if (style == null) { + // New style is missing, just clear it. + element.style = "" + } else if (typeof style !== "object") { + // New style is a string, let engine deal with patching. + element.style = style + } else if (old == null || typeof old !== "object") { + // `old` is missing or a string, `style` is an object. + element.style = "" + // Add new style properties + for (var key in style) { + var value = style[key] + if (value != null) { + if (key.includes("-")) element.style.setProperty(key, String(value)) + else element.style[key] = String(value) + } + } + } else { + // Both old & new are (different) objects. + // Remove style properties that no longer exist + // Style properties may have two cases(dash-case and camelCase), + // so removal should be done first to prevent accidental removal for newly setting values. + for (var key in old) { + if (old[key] != null && style[key] == null) { + if (key.includes("-")) element.style.removeProperty(key) + else element.style[key] = "" + } + } + // Update style properties that have changed + for (var key in style) { + var value = style[key] + if (value != null && (value = String(value)) !== String(old[key])) { + if (key.includes("-")) element.style.setProperty(key, value) + else element.style[key] = value + } + } + } + } + // Here's an explanation of how this works: + // 1. The event names are always (by design) prefixed by `on`. + // 2. The EventListener interface accepts either a function or an object + // with a `handleEvent` method. + // 3. The object does not inherit from `Object.prototype`, to avoid + // any potential interference with that (e.g. setters). + // 4. The event name is remapped to the handler before calling it. + // 5. In function-based event handlers, `ev.target === this`. We replicate + // that below. + // 6. In function-based event handlers, `return false` prevents the default + // action and stops event propagation. We replicate that below. + function EventDict() { + // Save this, so the current redraw is correctly tracked. + this._ = currentRedraw + } + EventDict.prototype = Object.create(null) + EventDict.prototype.handleEvent = function (ev) { + var handler = this["on" + ev.type] + var result + if (typeof handler === "function") result = handler.call(ev.currentTarget, ev) + else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) + var self = this + if (self._ != null) { + if (ev.redraw !== false) (0, self._)() + if (result != null && typeof result.then === "function") { + Promise.resolve(result).then(function () { + if (self._ != null && ev.redraw !== false) (0, self._)() + }) + } + } + if (result === false) { + ev.preventDefault() + ev.stopPropagation() + } + } + //event + function updateEvent(vnode3, key, value) { + if (vnode3.events != null) { + vnode3.events._ = currentRedraw + if (vnode3.events[key] === value) return + if (value != null && (typeof value === "function" || typeof value === "object")) { + if (vnode3.events[key] == null) vnode3.dom.addEventListener(key.slice(2), vnode3.events, false) + vnode3.events[key] = value + } else { + if (vnode3.events[key] != null) vnode3.dom.removeEventListener(key.slice(2), vnode3.events, false) + vnode3.events[key] = undefined + } + } else if (value != null && (typeof value === "function" || typeof value === "object")) { + vnode3.events = new EventDict() + vnode3.dom.addEventListener(key.slice(2), vnode3.events, false) + vnode3.events[key] = value + } + } + //lifecycle + function initLifecycle(source, vnode3, hooks) { + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode3) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode3)) + } + function updateLifecycle(source, vnode3, hooks) { + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode3)) + } + function shouldNotUpdate(vnode3, old) { + do { + if (vnode3.attrs != null && typeof vnode3.attrs.onbeforeupdate === "function") { + var force = callHook.call(vnode3.attrs.onbeforeupdate, vnode3, old) + if (force !== undefined && !force) break + } + if (typeof vnode3.tag !== "string" && typeof vnode3.state.onbeforeupdate === "function") { + var force = callHook.call(vnode3.state.onbeforeupdate, vnode3, old) + if (force !== undefined && !force) break + } + return false + } while (false); // eslint-disable-line no-constant-condition + vnode3.dom = old.dom + vnode3.domSize = old.domSize + vnode3.instance = old.instance + // One would think having the actual latest attributes would be ideal, + // but it doesn't let us properly diff based on our current internal + // representation. We have to save not only the old DOM info, but also + // the attributes used to create it, as we diff *that*, not against the + // DOM directly (with a few exceptions in `setAttr`). And, of course, we + // need to save the children and text as they are conceptually not + // unlike special "attributes" internally. + vnode3.attrs = old.attrs + vnode3.children = old.children + vnode3.text = old.text + return true + } + var currentDOM + return function(dom, vnodes, redraw) { + if (!dom) throw new TypeError("DOM element being rendered to does not exist.") + if (currentDOM != null && dom.contains(currentDOM)) { + throw new TypeError("Node is currently being rendered to and thus is locked.") + } + var prevRedraw = currentRedraw + var prevDOM = currentDOM + var hooks = [] + var active = activeElement(dom) + var namespace = dom.namespaceURI + currentDOM = dom + currentRedraw = typeof redraw === "function" ? redraw : undefined + currentRender = {} + try { + // First time rendering into a node clears it out + if (dom.vnodes == null) dom.textContent = "" + vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) + updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + dom.vnodes = vnodes + // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement + if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() + for (var i = 0; i < hooks.length; i++) hooks[i]() + } finally { + currentRedraw = prevRedraw + currentDOM = prevDOM + } + } +} +var render = _14() +var _21 = function(render2, schedule, console) { + var subscriptions = [] + var pending = false + var offset = -1 + function sync() { + for (offset = 0; offset < subscriptions.length; offset += 2) { + try { render2(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw) } + catch (e) { console.error(e) } + } + offset = -1 + } + function redraw() { + if (!pending) { + pending = true + schedule(function() { + pending = false + sync() + }) + } + } + redraw.sync = sync + function mount(root, component) { + if (component != null && component.view == null && typeof component !== "function") { + throw new TypeError("m.mount expects a component, not a vnode.") + } + var index = subscriptions.indexOf(root) + if (index >= 0) { + subscriptions.splice(index, 2) + if (index <= offset) offset -= 2 + render2(root, []) + } + if (component != null) { + subscriptions.push(root, component) + render2(root, Vnode(component), redraw) + } + } + return {mount: mount, redraw: redraw} +} +var mountRedraw = _21(render, typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : null, typeof console !== "undefined" ? console : null) +var buildQueryString = function(object) { + if (Object.prototype.toString.call(object) !== "[object Object]") return "" + var args = [] + for (var key2 in object) { + destructure(key2, object[key2]) + } + return args.join("&") + function destructure(key2, value1) { + if (Array.isArray(value1)) { + for (var i = 0; i < value1.length; i++) { + destructure(key2 + "[" + i + "]", value1[i]) + } + } + else if (Object.prototype.toString.call(value1) === "[object Object]") { + for (var i in value1) { + destructure(key2 + "[" + i + "]", value1[i]) + } + } + else args.push(encodeURIComponent(key2) + (value1 != null && value1 !== "" ? "=" + encodeURIComponent(value1) : "")) + } +} +// Returns `path` from `template` + `params` +var buildPathname = function(template, params) { + if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) { + throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") + } + if (params == null) return template + var queryIndex = template.indexOf("?") + var hashIndex = template.indexOf("#") + var queryEnd = hashIndex < 0 ? template.length : hashIndex + var pathEnd = queryIndex < 0 ? queryEnd : queryIndex + var path = template.slice(0, pathEnd) + var query = {} + Object.assign(query, params) + var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function(m3, key1, variadic) { + delete query[key1] + // If no such parameter exists, don't interpolate it. + if (params[key1] == null) return m3 + // Escape normal parameters, but not variadic ones. + return variadic ? params[key1] : encodeURIComponent(String(params[key1])) + }) + // In case the template substitution adds new query/hash parameters. + var newQueryIndex = resolved.indexOf("?") + var newHashIndex = resolved.indexOf("#") + var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex + var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex + var result0 = resolved.slice(0, newPathEnd) + if (queryIndex >= 0) result0 += template.slice(queryIndex, queryEnd) + if (newQueryIndex >= 0) result0 += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd) + var querystring = buildQueryString(query) + if (querystring) result0 += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring + if (hashIndex >= 0) result0 += template.slice(hashIndex) + if (newHashIndex >= 0) result0 += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex) + return result0 +} +var _25 = function($window, oncompletion) { + function PromiseProxy(executor) { + return new Promise(executor) + } + function makeRequest(url, args) { + return new Promise(function(resolve, reject) { + url = buildPathname(url, args.params) + var method = args.method != null ? args.method.toUpperCase() : "GET" + var body = args.body + var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData || body instanceof $window.URLSearchParams) + var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json") + var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false + var original0 = xhr, replacedAbort + var abort = xhr.abort + xhr.abort = function() { + aborted = true + abort.call(this) + } + xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) + if (assumeJSON && body != null && !hasHeader(args, "content-type")) { + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") + } + if (typeof args.deserialize !== "function" && !hasHeader(args, "accept")) { + xhr.setRequestHeader("Accept", "application/json, text/*") + } + if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout + xhr.responseType = responseType + for (var key0 in args.headers) { + if (hasOwn.call(args.headers, key0)) { + xhr.setRequestHeader(key0, args.headers[key0]) + } + } + xhr.onreadystatechange = function(ev) { + // Don't throw errors on xhr.abort(). + if (aborted) return + if (ev.target.readyState === 4) { + try { + var success = (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || (/^file:\/\//i).test(url) + // When the response type isn't "" or "text", + // `xhr.responseText` is the wrong thing to use. + // Browsers do the right thing and throw here, and we + // should honor that and do the right thing by + // preferring `xhr.response` where possible/practical. + var response = ev.target.response, message + if (responseType === "json") { + // For IE and Edge, which don't implement + // `responseType: "json"`. + if (!ev.target.responseType && typeof args.extract !== "function") { + // Handle no-content which will not parse. + try { response = JSON.parse(ev.target.responseText) } + catch (e) { response = null } + } + } else if (!responseType || responseType === "text") { + // Only use this default if it's text. If a parsed + // document is needed on old IE and friends (all + // unsupported), the user should use a custom + // `config` instead. They're already using this at + // their own risk. + if (response == null) response = ev.target.responseText + } + if (typeof args.extract === "function") { + response = args.extract(ev.target, args) + success = true + } else if (typeof args.deserialize === "function") { + response = args.deserialize(response) + } + if (success) { + if (typeof args.type === "function") { + if (Array.isArray(response)) { + for (var i = 0; i < response.length; i++) { + response[i] = new args.type(response[i]) + } + } + else response = new args.type(response) + } + resolve(response) + } + else { + var completeErrorResponse = function() { + try { message = ev.target.responseText } + catch (e) { message = response } + var error = new Error(message) + error.code = ev.target.status + error.response = response + reject(error) + } + if (xhr.status === 0) { + // Use setTimeout to push this code block onto the event queue + // This allows `xhr.ontimeout` to run in the case that there is a timeout + // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject + // as `xhr.onreadystatechange` will run before it + setTimeout(function() { + if (isTimeout) return + completeErrorResponse() + }) + } else completeErrorResponse() + } + } + catch (e) { + reject(e) + } + } + } + xhr.ontimeout = function (ev) { + isTimeout = true + var error = new Error("Request timed out") + error.code = ev.target.status + reject(error) + } + if (typeof args.config === "function") { + xhr = args.config(xhr, args, url) || xhr + // Propagate the `abort` to any replacement XHR as well. + if (xhr !== original0) { + replacedAbort = xhr.abort + xhr.abort = function() { + aborted = true + replacedAbort.call(this) + } + } + } + if (body == null) xhr.send() + else if (typeof args.serialize === "function") xhr.send(args.serialize(body)) + else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) xhr.send(body) + else xhr.send(JSON.stringify(body)) + }) + } + // In case the global Promise is some userland library's where they rely on + // `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or + // similar. Let's *not* break them. + PromiseProxy.prototype = Promise.prototype + PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto + function hasHeader(args, name) { + for (var key0 in args.headers) { + if (hasOwn.call(args.headers, key0) && key0.toLowerCase() === name) return true + } + return false + } + return { + request: function(url, args) { + if (typeof url !== "string") { args = url; url = url.url } + else if (args == null) args = {} + var promise = makeRequest(url, args) + if (args.background === true) return promise + var count = 0 + function complete() { + if (--count === 0 && typeof oncompletion === "function") oncompletion() + } + return wrap(promise) + function wrap(promise) { + var then = promise.then + // Set the constructor, so engines know to not await or resolve + // this as a native promise. At the time of writing, this is + // only necessary for V8, but their behavior is the correct + // behavior per spec. See this spec issue for more details: + // https://github.com/tc39/ecma262/issues/1577. Also, see the + // corresponding comment in `request/tests/test-request.js` for + // a bit more background on the issue at hand. + promise.constructor = PromiseProxy + promise.then = function() { + count++ + var next = then.apply(promise, arguments) + next.then(complete, function(e) { + complete() + if (count === 0) throw e + }) + return wrap(next) + } + return promise + } + } + } +} +var request = _25(typeof window !== "undefined" ? window : null, mountRedraw.redraw) +/* +Percent encodings encode UTF-8 bytes, so this regexp needs to match that. +Here's how UTF-8 encodes stuff: +- `00-7F`: 1-byte, for U+0000-U+007F +- `C2-DF 80-BF`: 2-byte, for U+0080-U+07FF +- `E0-EF 80-BF 80-BF`: 3-byte, encodes U+0800-U+FFFF +- `F0-F4 80-BF 80-BF 80-BF`: 4-byte, encodes U+10000-U+10FFFF +In this, there's a number of invalid byte sequences: +- `80-BF`: Continuation byte, invalid as start +- `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F +- `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF +- `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF +- `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF +- `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +- `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +So in reality, only the following sequences can encode are valid characters: +- 00-7F +- C2-DF 80-BF +- E0 A0-BF 80-BF +- E1-EC 80-BF 80-BF +- ED 80-9F 80-BF +- EE-EF 80-BF 80-BF +- F0 90-BF 80-BF 80-BF +- F1-F3 80-BF 80-BF 80-BF +- F4 80-8F 80-BF 80-BF +The regexp just tries to match this as compactly as possible. +*/ +var validUtf8Encodings = /%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi +var decodeURIComponentSafe = function(str) { + return String(str).replace(validUtf8Encodings, decodeURIComponent) +} +var parseQueryString = function(string) { + if (string === "" || string == null) return {} + if (string.charAt(0) === "?") string = string.slice(1) + var entries = string.split("&"), counters = {}, data0 = {} + for (var i = 0; i < entries.length; i++) { + var entry = entries[i].split("=") + var key4 = decodeURIComponentSafe(entry[0]) + var value2 = entry.length === 2 ? decodeURIComponentSafe(entry[1]) : "" + if (value2 === "true") value2 = true + else if (value2 === "false") value2 = false + var levels = key4.split(/\]\[?|\[/) + var cursor = data0 + if (key4.indexOf("[") > -1) levels.pop() + for (var j0 = 0; j0 < levels.length; j0++) { + var level = levels[j0], nextLevel = levels[j0 + 1] + var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) + if (level === "") { + var key4 = levels.slice(0, j0).join() + if (counters[key4] == null) { + counters[key4] = Array.isArray(cursor) ? cursor.length : 0 + } + level = counters[key4]++ + } + // Disallow direct prototype pollution + else if (level === "__proto__") break + if (j0 === levels.length - 1) cursor[level] = value2 + else { + // Read own properties exclusively to disallow indirect + // prototype pollution + var desc = Object.getOwnPropertyDescriptor(cursor, level) + if (desc != null) desc = desc.value + if (desc == null) cursor[level] = desc = isNumber ? [] : {} + cursor = desc + } + } + } + return data0 +} +// Returns `{path, params}` from `url` +var parsePathname = function(url) { + var queryIndex0 = url.indexOf("?") + var hashIndex0 = url.indexOf("#") + var queryEnd0 = hashIndex0 < 0 ? url.length : hashIndex0 + var pathEnd0 = queryIndex0 < 0 ? queryEnd0 : queryIndex0 + var path1 = url.slice(0, pathEnd0).replace(/\/{2,}/g, "/") + if (!path1) path1 = "/" + else { + if (path1[0] !== "/") path1 = "/" + path1 + } + return { + path: path1, + params: queryIndex0 < 0 + ? {} + : parseQueryString(url.slice(queryIndex0 + 1, queryEnd0)), + } +} +// Compiles a template into a function that takes a resolved path (without query +// strings) and returns an object containing the template parameters with their +// parsed values. This expects the input of the compiled template to be the +// output of `parsePathname`. Note that it does *not* remove query parameters +// specified in the template. +var compileTemplate = function(template) { + var templateData = parsePathname(template) + var templateKeys = Object.keys(templateData.params) + var keys = [] + var regexp = new RegExp("^" + templateData.path.replace( + // I escape literal text so people can use things like `:file.:ext` or + // `:lang-:locale` in routes. This is all merged into one pass so I + // don't also accidentally escape `-` and make it harder to detect it to + // ban it from template parameters. + /:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g, + function(m4, key5, extra) { + if (key5 == null) return "\\" + m4 + keys.push({k: key5, r: extra === "..."}) + if (extra === "...") return "(.*)" + if (extra === ".") return "([^/]+)\\." + return "([^/]+)" + (extra || "") + } + ) + "\\/?$") + return function(data1) { + // First, check the params. Usually, there isn't any, and it's just + // checking a static set. + for (var i = 0; i < templateKeys.length; i++) { + if (templateData.params[templateKeys[i]] !== data1.params[templateKeys[i]]) return false + } + // If no interpolations exist, let's skip all the ceremony + if (!keys.length) return regexp.test(data1.path) + var values = regexp.exec(data1.path) + if (values == null) return false + for (var i = 0; i < keys.length; i++) { + data1.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1]) + } + return true + } +} +// Note: this is mildly perf-sensitive. +// +// It does *not* use `delete` - dynamic `delete`s usually cause objects to bail +// out into dictionary mode and just generally cause a bunch of optimization +// issues within engines. +// +// Ideally, I would've preferred to do this, if it weren't for the optimization +// issues: +// +// ```js +// const hasOwn = hasOwn +// const magic = [ +// "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", +// "onbeforeremove", "onremove", +// ] +// var censor = (attrs, extras) => { +// const result = Object.assign(Object.create(null), attrs) +// for (const key of magic) delete result[key] +// if (extras != null) for (const key of extras) delete result[key] +// return result +// } +// ``` +var magic = /^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$/ +var censor = function(attrs7, extras) { + var result2 = {} + if (extras != null) { + for (var key6 in attrs7) { + if (hasOwn.call(attrs7, key6) && !magic.test(key6) && extras.indexOf(key6) < 0) { + result2[key6] = attrs7[key6] + } + } + } else { + for (var key6 in attrs7) { + if (hasOwn.call(attrs7, key6) && !magic.test(key6)) { + result2[key6] = attrs7[key6] + } + } + } + return result2 +} +var _31 = function($window, mountRedraw0) { + var p = Promise.resolve() + var scheduled = false + var ready = false + var hasBeenResolved = false + var dom0, compiled, fallbackRoute + var currentResolver, component, attrs6, currentPath, lastUpdate + var RouterRoot = { + onremove: function() { + ready = hasBeenResolved = false + $window.removeEventListener("popstate", fireAsync, false) + }, + view: function() { + // The route has already been resolved. + // Therefore, the following early return is not needed. + // if (!hasBeenResolved) return + var vnode6 = Vnode(component, attrs6.key, attrs6) + if (currentResolver) return currentResolver.render(vnode6) + // Wrap in a fragment to preserve existing key semantics + return [vnode6] + }, + } + var SKIP = route.SKIP = {} + function resolveRoute() { + scheduled = false + // Consider the pathname holistically. The prefix might even be invalid, + // but that's not our problem. + var prefix = $window.location.hash + if (route.prefix[0] !== "#") { + prefix = $window.location.search + prefix + if (route.prefix[0] !== "?") { + prefix = $window.location.pathname + prefix + if (prefix[0] !== "/") prefix = "/" + prefix + } + } + var path0 = decodeURIComponentSafe(prefix).slice(route.prefix.length) + var data = parsePathname(path0) + Object.assign(data.params, $window.history.state) + function reject(e) { + console.error(e) + route.set(fallbackRoute, null, {replace: true}) + } + loop(0) + function loop(i) { + for (; i < compiled.length; i++) { + if (compiled[i].check(data)) { + var payload = compiled[i].component + var matchedRoute = compiled[i].route + var localComp = payload + var update = lastUpdate = function(comp) { + if (update !== lastUpdate) return + if (comp === SKIP) return loop(i + 1) + component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + attrs6 = data.params, currentPath = path0, lastUpdate = null + currentResolver = payload.render ? payload : null + if (hasBeenResolved) mountRedraw0.redraw() + else { + hasBeenResolved = true + mountRedraw0.mount(dom0, RouterRoot) + } + } + // There's no understating how much I *wish* I could + // use `async`/`await` here... + if (payload.view || typeof payload === "function") { + payload = {} + update(localComp) + } + else if (payload.onmatch) { + p.then(function () { + return payload.onmatch(data.params, path0, matchedRoute) + }).then(update, path0 === fallbackRoute ? null : reject) + } + else update(/* "div" */) + return + } + } + if (path0 === fallbackRoute) { + throw new Error("Could not resolve default route " + fallbackRoute + ".") + } + route.set(fallbackRoute, null, {replace: true}) + } + } + function fireAsync() { + if (!scheduled) { + scheduled = true + // TODO: just do `mountRedraw.redraw()` here and elide the timer + // dependency. Note that this will muck with tests a *lot*, so it's + // not as easy of a change as it sounds. + setTimeout(resolveRoute) + } + } + function route(root, defaultRoute, routes) { + if (!root) throw new TypeError("DOM element being rendered to does not exist.") + compiled = Object.keys(routes).map(function(route) { + if (route[0] !== "/") throw new SyntaxError("Routes must start with a '/'.") + if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { + throw new SyntaxError("Route parameter names must be separated with either '/', '.', or '-'.") + } + return { + route: route, + component: routes[route], + check: compileTemplate(route), + } + }) + fallbackRoute = defaultRoute + if (defaultRoute != null) { + var defaultData = parsePathname(defaultRoute) + if (!compiled.some(function (i) { return i.check(defaultData) })) { + throw new ReferenceError("Default route doesn't match any known routes.") + } + } + dom0 = root + $window.addEventListener("popstate", fireAsync, false) + ready = true + // The RouterRoot component is mounted when the route is first resolved. + resolveRoute() + } + route.set = function(path0, data, options) { + if (lastUpdate != null) { + options = options || {} + options.replace = true + } + lastUpdate = null + path0 = buildPathname(path0, data) + if (ready) { + fireAsync() + var state = options ? options.state : null + var title = options ? options.title : null + if (options && options.replace) $window.history.replaceState(state, title, route.prefix + path0) + else $window.history.pushState(state, title, route.prefix + path0) + } + else { + $window.location.href = route.prefix + path0 + } + } + route.get = function() {return currentPath} + route.prefix = "#!" + route.Link = { + view: function(vnode6) { + // Omit the used parameters from the rendered element - they are + // internal. Also, censor the various lifecycle methods. + // + // We don't strip the other parameters because for convenience we + // let them be specified in the selector as well. + var child0 = hyperscript( + vnode6.attrs.selector || "a", + censor(vnode6.attrs, ["options", "params", "selector", "onclick"]), + vnode6.children + ) + var options, onclick, href + // Let's provide a *right* way to disable a route link, rather than + // letting people screw up accessibility on accident. + // + // The attribute is coerced so users don't get surprised over + // `disabled: 0` resulting in a button that's somehow routable + // despite being visibly disabled. + if (child0.attrs.disabled = Boolean(child0.attrs.disabled)) { + child0.attrs.href = null + child0.attrs["aria-disabled"] = "true" + // If you *really* do want add `onclick` on a disabled link, use + // an `oncreate` hook to add it. + } else { + options = vnode6.attrs.options + onclick = vnode6.attrs.onclick + // Easier to build it now to keep it isomorphic. + href = buildPathname(child0.attrs.href, vnode6.attrs.params) + child0.attrs.href = route.prefix + href + child0.attrs.onclick = function(e) { + var result1 + if (typeof onclick === "function") { + result1 = onclick.call(e.currentTarget, e) + } else if (onclick == null || typeof onclick !== "object") { + // do nothing + } else if (typeof onclick.handleEvent === "function") { + onclick.handleEvent(e) + } + // Adapted from React Router's implementation: + // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js + // + // Try to be flexible and intuitive in how we handle links. + // Fun fact: links aren't as obvious to get right as you + // would expect. There's a lot more valid ways to click a + // link than this, and one might want to not simply click a + // link, but right click or command-click it to copy the + // link target, etc. Nope, this isn't just for blind people. + if ( + // Skip if `onclick` prevented default + result1 !== false && !e.defaultPrevented && + // Ignore everything but left clicks + (e.button === 0 || e.which === 0 || e.which === 1) && + // Let the browser handle `target=_blank`, etc. + (!e.currentTarget.target || e.currentTarget.target === "_self") && + // No modifier keys + !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey + ) { + e.preventDefault() + e.redraw = false + route.set(href, null, options) + } + } + } + return child0 + }, + } + route.param = function(key3) { + return attrs6 && key3 != null ? attrs6[key3] : attrs6 + } + return route +} +var router = _31(typeof window !== "undefined" ? window : null, mountRedraw) +var m = function m() { return hyperscript.apply(this, arguments) } +m.m = hyperscript +m.trust = hyperscript.trust +m.fragment = hyperscript.fragment +m.Fragment = "[" +m.mount = mountRedraw.mount +m.route = router +m.render = render +m.redraw = mountRedraw.redraw +m.request = request.request +m.parseQueryString = parseQueryString +m.buildQueryString = buildQueryString +m.parsePathname = parsePathname +m.buildPathname = buildPathname +m.vnode = Vnode +m.censor = censor +m.domFor = domFor +if (typeof module !== "undefined") module["exports"] = m +else window.m = m +}()); \ No newline at end of file diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 3521bd3..b3c3885 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -45,6 +45,19 @@ const Editor = { this.generateUrl = vnode.attrs.generateUrl; this.logsUrl = vnode.attrs.logsUrl; // Needed for debug feature + // Editor Features State + this.isFullScreen = false; + this.editorTheme = localStorage.getItem('editorTheme') || 'auto'; + this.availableThemes = [ + { id: 'auto', name: 'Auto' }, + { id: 'monokai', name: 'Monokai' }, + { id: 'chrome', name: 'Chrome' }, + { id: 'github', name: 'GitHub' }, + { id: 'tomorrow', name: 'Tomorrow' }, + { id: 'twilight', name: 'Twilight' }, + { id: 'dracula', name: 'Dracula' } + ]; + // AI State this.naturalLanguageQuery = ""; this.aiLoading = false; // General AI loading state @@ -56,13 +69,23 @@ const Editor = { oncreate() { this.editorJS = ace.edit("js-editor"); - this.editorJS.setOptions({ maxLines: 100 }); - // Determine initial theme - const isDark = document.documentElement.classList.contains('dark'); - const theme = isDark ? "ace/theme/monokai" : "ace/theme/chrome"; + // safe require of language tools + try { + ace.require("ace/ext/language_tools"); + } catch (e) { + console.warn("Ace language tools not loaded"); + } + + this.editorJS.setOptions({ + minLines: this.isFullScreen ? undefined : 15, + maxLines: this.isFullScreen ? undefined : 100, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }); - this.editorJS.setTheme(theme); + this.applyTheme(); + this.editorJS.session.setMode( this.runtime === "python" ? "ace/mode/python" : "ace/mode/javascript" ); @@ -74,8 +97,12 @@ const Editor = { }); this.editorJSON = ace.edit("json-editor"); - this.editorJSON.setOptions({ maxLines: 100 }); - this.editorJSON.setTheme(theme); + this.editorJSON.setOptions({ + minLines: 15, + maxLines: 100, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }); this.editorJSON.session.setMode("ace/mode/json"); this.editorJSON.setValue(this.jsonValue, -1); @@ -86,17 +113,68 @@ const Editor = { // Listen for theme changes this.themeListener = (e) => { - const newTheme = e.detail.theme === 'dark' ? "ace/theme/monokai" : "ace/theme/chrome"; - this.editorJS.setTheme(newTheme); - this.editorJSON.setTheme(newTheme); + if (this.editorTheme === 'auto') { + this.applyTheme(); + } }; window.addEventListener('themeChanged', this.themeListener); + + // Listen for Escape key to exit full screen + this.escListener = (e) => { + if (this.isFullScreen && e.key === 'Escape') { + this.toggleFullScreen(); + m.redraw(); + } + }; + document.addEventListener('keydown', this.escListener); + }, + + applyTheme() { + let themeToSet = this.editorTheme; + if (themeToSet === 'auto') { + const isDark = document.documentElement.classList.contains('dark'); + themeToSet = isDark ? 'monokai' : 'chrome'; + } + const aceTheme = `ace/theme/${themeToSet}`; + if (this.editorJS) this.editorJS.setTheme(aceTheme); + if (this.editorJSON) this.editorJSON.setTheme(aceTheme); + }, + + toggleFullScreen() { + this.isFullScreen = !this.isFullScreen; + if (this.isFullScreen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + // Wait for Mithril to redraw so the DOM is updated (classes/styles applied) + // before resizing the editor. + setTimeout(() => { + if (this.editorJS) { + this.editorJS.setOptions({ + minLines: this.isFullScreen ? undefined : 15, + maxLines: this.isFullScreen ? undefined : 100 + }); + this.editorJS.resize(); + } + }, 0); + }, + + setTheme(themeId) { + this.editorTheme = themeId; + localStorage.setItem('editorTheme', themeId); + this.applyTheme(); }, onremove() { if (this.themeListener) { window.removeEventListener('themeChanged', this.themeListener); } + if (this.escListener) { + document.removeEventListener('keydown', this.escListener); + } + document.body.style.overflow = ''; }, async execute() { @@ -298,12 +376,19 @@ const Editor = { }, view() { - return m("div", { class: "" }, [ + const fullScreenStyle = this.isFullScreen + ? { position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh", zIndex: "9999", backgroundColor: this.editorTheme === 'monokai' || this.editorTheme === 'twilight' || this.editorTheme === 'dracula' ? '#1a1a1a' : '#ffffff' } + : {}; + + return m("div", { + class: this.isFullScreen ? "flex flex-col" : "", + style: fullScreenStyle + }, [ /* Header */ m( "div", { - class: "flex items-center justify-between pl-2", + class: "flex items-center justify-between pl-2 " + (this.isFullScreen ? "p-4 border-b border-gray-200 dark:border-gray-800" : ""), }, [ this.showHeader @@ -417,7 +502,7 @@ const Editor = { : null, ]) : m("div"), - m("div"), + m("div", { class: "flex items-center space-x-2" }), ] ), @@ -473,7 +558,35 @@ const Editor = { this.aiLoading && m("div", {class: "animate-spin h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full ml-2"}) ]), - m("div", { class: "flex items-center space-x-4" }, [ + m("div", { class: "flex items-center space-x-2" }, [ + // Theme Selector + m("select", { + key: "theme-selector", + class: "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", + value: this.editorTheme, + onchange: (e) => this.setTheme(e.target.value) + }, this.availableThemes.map(theme => + m("option", { value: theme.id }, theme.name) + )), + + // Full Screen Toggle + m("button", { + key: "fullscreen-toggle", + class: "p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400", + onclick: () => this.toggleFullScreen(), + title: this.isFullScreen ? "Exit Full Screen (Esc)" : "Full Screen" + }, m("svg", { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + stroke: "currentColor", + "stroke-width": 2, + class: "w-5 h-5" + }, this.isFullScreen + ? m("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" }) + : m("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" }) + )), + m( "select", { @@ -570,7 +683,9 @@ const Editor = { ] ), - m("div", { id: "js-editor", class: "rounded shadow h-64" }), + m("div", { class: "relative rounded shadow " + (this.isFullScreen ? "flex-1" : "") }, [ + m("div", { id: "js-editor", style: "width: 100%; height: 100%;" }) + ]), m( "div", @@ -607,7 +722,9 @@ const Editor = { ] ), - m("div", { id: "json-editor", class: "rounded shadow h-64" }), + m("div", { class: "relative rounded shadow" }, [ + m("div", { id: "json-editor", style: "width: 100%; height: 100%;" }) + ]), m("input", { type: "hidden", name: "script", value: this.jsValue }), m("input", { diff --git a/templates/base.html b/templates/base.html index db61938..4808b81 100644 --- a/templates/base.html +++ b/templates/base.html @@ -28,8 +28,10 @@ crossorigin="anonymous" referrerpolicy="no-referrer"> + - +