"use strict"; // https://gomakethings.com/finding-the-next-and-previous-sibling-elements-that-match-a-selector-with-vanilla-js/ function getNextSibling(elem, selector) { // Get the next sibling element let sibling = elem.nextElementSibling // If there's no selector, return the first sibling if (!selector) { return sibling } // If the sibling matches our selector, use it // If not, jump to the next sibling and continue the loop while (sibling) { if (sibling.matches(selector)) { return sibling } sibling = sibling.nextElementSibling } } /* Panel Stuff */ // true = open let COLLAPSIBLES_INITIALIZED = false; const COLLAPSIBLES_KEY = "collapsibles"; const COLLAPSIBLE_PANELS = []; // filled in by createCollapsibles with all the elements matching .collapsible // on-init call this for any panels that are marked open function toggleCollapsible(element) { const collapsibleHeader = element.querySelector(".collapsible"); const handle = element.querySelector(".collapsible-handle"); collapsibleHeader.classList.toggle("active") let content = getNextSibling(collapsibleHeader, '.collapsible-content') if (!collapsibleHeader.classList.contains("active")) { content.style.display = "none" if (handle != null) { // render results don't have a handle handle.innerHTML = '➕' // plus } } else { content.style.display = "block" if (handle != null) { // render results don't have a handle handle.innerHTML = '➖' // minus } } document.dispatchEvent(new CustomEvent('collapsibleClick', { detail: collapsibleHeader })) if (COLLAPSIBLES_INITIALIZED && COLLAPSIBLE_PANELS.includes(element)) { saveCollapsibles() } } function saveCollapsibles() { let values = {} COLLAPSIBLE_PANELS.forEach(element => { let value = element.querySelector(".collapsible").className.indexOf("active") !== -1 values[element.id] = value }) localStorage.setItem(COLLAPSIBLES_KEY, JSON.stringify(values)) } function createCollapsibles(node) { let save = false if (!node) { node = document save = true } let collapsibles = node.querySelectorAll(".collapsible") collapsibles.forEach(function(c) { if (save && c.parentElement.id) { COLLAPSIBLE_PANELS.push(c.parentElement) } let handle = document.createElement('span') handle.className = 'collapsible-handle' if (c.classList.contains("active")) { handle.innerHTML = '➖' // minus } else { handle.innerHTML = '➕' // plus } c.insertBefore(handle, c.firstChild) c.addEventListener('click', function() { toggleCollapsible(c.parentElement) }) }) if (save) { let saved = localStorage.getItem(COLLAPSIBLES_KEY) if (!saved) { saved = tryLoadOldCollapsibles(); } if (!saved) { saveCollapsibles() saved = localStorage.getItem(COLLAPSIBLES_KEY) } let values = JSON.parse(saved) COLLAPSIBLE_PANELS.forEach(element => { let value = element.querySelector(".collapsible").className.indexOf("active") !== -1 if (values[element.id] != value) { toggleCollapsible(element) } }) COLLAPSIBLES_INITIALIZED = true } } function tryLoadOldCollapsibles() { const old_map = { "advancedPanelOpen": "editor-settings", "modifiersPanelOpen": "editor-modifiers", "negativePromptPanelOpen": "editor-inputs-prompt" }; if (localStorage.getItem(Object.keys(old_map)[0])) { let result = {}; Object.keys(old_map).forEach(key => { const value = localStorage.getItem(key); if (value !== null) { result[old_map[key]] = (value == true || value == "true") localStorage.removeItem(key) } }); result = JSON.stringify(result) localStorage.setItem(COLLAPSIBLES_KEY, result) return result } return null; } function permute(arr) { let permutations = [] let n = arr.length let n_permutations = Math.pow(2, n) for (let i = 0; i < n_permutations; i++) { let perm = [] let mask = Number(i).toString(2).padStart(n, '0') for (let idx = 0; idx < mask.length; idx++) { if (mask[idx] === '1' && arr[idx].trim() !== '') { perm.push(arr[idx]) } } if (perm.length > 0) { permutations.push(perm) } } return permutations } // https://stackoverflow.com/a/8212878 function millisecondsToStr(milliseconds) { function numberEnding (number) { return (number > 1) ? 's' : '' } let temp = Math.floor(milliseconds / 1000) let hours = Math.floor((temp %= 86400) / 3600) let s = '' if (hours) { s += hours + ' hour' + numberEnding(hours) + ' ' } let minutes = Math.floor((temp %= 3600) / 60) if (minutes) { s += minutes + ' minute' + numberEnding(minutes) + ' ' } let seconds = temp % 60 if (!hours && minutes < 4 && seconds) { s += seconds + ' second' + numberEnding(seconds) } return s } // https://rosettacode.org/wiki/Brace_expansion#JavaScript function BraceExpander() { 'use strict' // Index of any closing brace matching the opening // brace at iPosn, // with the indices of any immediately-enclosed commas. function bracePair(tkns, iPosn, iNest, lstCommas) { if (iPosn >= tkns.length || iPosn < 0) return null; let t = tkns[iPosn], n = (t === '{') ? ( iNest + 1 ) : (t === '}' ? ( iNest - 1 ) : iNest), lst = (t === ',' && iNest === 1) ? ( lstCommas.concat(iPosn) ) : lstCommas; return n ? bracePair(tkns, iPosn + 1, n, lst) : { close: iPosn, commas: lst }; } // Parse of a SYNTAGM subtree function andTree(dctSofar, tkns) { if (!tkns.length) return [dctSofar, []]; let dctParse = dctSofar ? dctSofar : { fn: and, args: [] }, head = tkns[0], tail = head ? tkns.slice(1) : [], dctBrace = head === '{' ? bracePair( tkns, 0, 0, [] ) : null, lstOR = dctBrace && ( dctBrace.close ) && dctBrace.commas.length ? ( splitAt(dctBrace.close + 1, tkns) ) : null; return andTree({ fn: and, args: dctParse.args.concat( lstOR ? ( orTree(dctParse, lstOR[0], dctBrace.commas) ) : head ) }, lstOR ? ( lstOR[1] ) : tail); } // Parse of a PARADIGM subtree function orTree(dctSofar, tkns, lstCommas) { if (!tkns.length) return [dctSofar, []]; let iLast = lstCommas.length; return { fn: or, args: splitsAt( lstCommas, tkns ).map(function (x, i) { let ts = x.slice( 1, i === iLast ? ( -1 ) : void 0 ); return ts.length ? ts : ['']; }).map(function (ts) { return ts.length > 1 ? ( andTree(null, ts)[0] ) : ts[0]; }) }; } // List of unescaped braces and commas, and remaining strings function tokens(str) { // Filter function excludes empty splitting artefacts let toS = function (x) { return x.toString(); }; return str.split(/(\\\\)/).filter(toS).reduce(function (a, s) { return a.concat(s.charAt(0) === '\\' ? s : s.split( /(\\*[{,}])/ ).filter(toS)); }, []); } // PARSE TREE OPERATOR (1 of 2) // Each possible head * each possible tail function and(args) { let lng = args.length, head = lng ? args[0] : null, lstHead = "string" === typeof head ? ( [head] ) : head; return lng ? ( 1 < lng ? lstHead.reduce(function (a, h) { return a.concat( and(args.slice(1)).map(function (t) { return h + t; }) ); }, []) : lstHead ) : []; } // PARSE TREE OPERATOR (2 of 2) // Each option flattened function or(args) { return args.reduce(function (a, b) { return a.concat(b); }, []); } // One list split into two (first sublist length n) function splitAt(n, lst) { return n < lst.length + 1 ? [ lst.slice(0, n), lst.slice(n) ] : [lst, []]; } // One list split into several (sublist lengths [n]) function splitsAt(lstN, lst) { return lstN.reduceRight(function (a, x) { return splitAt(x, a[0]).concat(a.slice(1)); }, [lst]); } // Value of the parse tree function evaluated(e) { return typeof e === 'string' ? e : e.fn(e.args.map(evaluated)); } // JSON prettyprint (for parse tree, token list etc) function pp(e) { return JSON.stringify(e, function (k, v) { return typeof v === 'function' ? ( '[function ' + v.name + ']' ) : v; }, 2) } // ----------------------- MAIN ------------------------ // s -> [s] this.expand = function(s) { // BRACE EXPRESSION PARSED let dctParse = andTree(null, tokens(s))[0]; // ABSTRACT SYNTAX TREE LOGGED // console.log(pp(dctParse)); // AST EVALUATED TO LIST OF STRINGS return evaluated(dctParse); } } /** Pause the execution of an async function until timer elapse. * @Returns a promise that will resolve after the specified timeout. */ function asyncDelay(timeout) { return new Promise(function(resolve, reject) { setTimeout(resolve, timeout, true) }) } function PromiseSource() { const srcPromise = new Promise((resolve, reject) => { Object.defineProperties(this, { resolve: { value: resolve, writable: false } , reject: { value: reject, writable: false } }) }) Object.defineProperties(this, { promise: {value: makeQuerablePromise(srcPromise), writable: false} }) } /** A debounce is a higher-order function, which is a function that returns another function * that, as long as it continues to be invoked, will not be triggered. * The function will be called after it stops being called for N milliseconds. * If `immediate` is passed, trigger the function on the leading edge, instead of the trailing. * @Returns a promise that will resolve to func return value. */ function debounce (func, wait, immediate) { if (typeof wait === "undefined") { wait = 40 } if (typeof wait !== "number") { throw new Error("wait is not an number.") } let timeout = null let lastPromiseSrc = new PromiseSource() const applyFn = function(context, args) { let result = undefined try { result = func.apply(context, args) } catch (err) { lastPromiseSrc.reject(err) } if (result instanceof Promise) { result.then(lastPromiseSrc.resolve, lastPromiseSrc.reject) } else { lastPromiseSrc.resolve(result) } } return function(...args) { const callNow = Boolean(immediate && !timeout) const context = this; if (timeout) { clearTimeout(timeout) } timeout = setTimeout(function () { if (!immediate) { applyFn(context, args) } lastPromiseSrc = new PromiseSource() timeout = null }, wait) if (callNow) { applyFn(context, args) } return lastPromiseSrc.promise } } function preventNonNumericalInput(e) { e = e || window.event; let charCode = (typeof e.which == "undefined") ? e.keyCode : e.which; let charStr = String.fromCharCode(charCode); let re = e.target.getAttribute('pattern') || '^[0-9]+$' re = new RegExp(re) if (!charStr.match(re)) { e.preventDefault(); } } /** Returns the global object for the current execution environement. * @Returns window in a browser, global in node and self in a ServiceWorker. * @Notes Allows unit testing and use of the engine outside of a browser. */ function getGlobal() { if (typeof globalThis === 'object') { return globalThis } else if (typeof global === 'object') { return global } else if (typeof self === 'object') { return self } try { return Function('return this')() } catch { // If the Function constructor fails, we're in a browser with eval disabled by CSP headers. return window } // Returns undefined if global can't be found. } /** Check if x is an Array or a TypedArray. * @Returns true if x is an Array or a TypedArray, false otherwise. */ function isArrayOrTypedArray(x) { return Boolean(typeof x === 'object' && (Array.isArray(x) || (ArrayBuffer.isView(x) && !(x instanceof DataView)))) } function makeQuerablePromise(promise) { if (typeof promise !== 'object') { throw new Error('promise is not an object.') } if (!(promise instanceof Promise)) { throw new Error('Argument is not a promise.') } // Don't modify a promise that's been already modified. if ('isResolved' in promise || 'isRejected' in promise || 'isPending' in promise) { return promise } let isPending = true let isRejected = false let rejectReason = undefined let isResolved = false let resolvedValue = undefined const qurPro = promise.then( function(val){ isResolved = true isPending = false resolvedValue = val return val } , function(reason) { rejectReason = reason isRejected = true isPending = false throw reason } ) Object.defineProperties(qurPro, { 'isResolved': { get: () => isResolved } , 'resolvedValue': { get: () => resolvedValue } , 'isPending': { get: () => isPending } , 'isRejected': { get: () => isRejected } , 'rejectReason': { get: () => rejectReason } }) return qurPro } /* inserts custom html to allow prettifying of inputs */ function prettifyInputs(root_element) { root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => { if (element.style.display === "none") { return } var parent = element.parentNode; if (!parent.classList.contains("input-toggle")) { var wrapper = document.createElement("div"); wrapper.classList.add("input-toggle"); parent.replaceChild(wrapper, element); wrapper.appendChild(element); var label = document.createElement("label"); label.htmlFor = element.id; wrapper.appendChild(label); } }) } class GenericEventSource { #events = {}; #types = [] constructor(...eventsTypes) { if (Array.isArray(eventsTypes) && eventsTypes.length === 1 && Array.isArray(eventsTypes[0])) { eventsTypes = eventsTypes[0] } this.#types.push(...eventsTypes) } get eventTypes() { return this.#types } /** Add a new event listener */ addEventListener(name, handler) { if (!this.#types.includes(name)) { throw new Error('Invalid event name.') } if (this.#events.hasOwnProperty(name)) { this.#events[name].push(handler) } else { this.#events[name] = [handler] } } /** Remove the event listener */ removeEventListener(name, handler) { if (!this.#events.hasOwnProperty(name)) { return } const index = this.#events[name].indexOf(handler) if (index != -1) { this.#events[name].splice(index, 1) } } fireEvent(name, ...args) { if (!this.#types.includes(name)) { throw new Error(`Event ${String(name)} missing from Events.types`) } if (!this.#events.hasOwnProperty(name)) { return Promise.resolve() } if (!args || !args.length) { args = [] } const evs = this.#events[name] if (evs.length <= 0) { return Promise.resolve() } return Promise.allSettled(evs.map((callback) => { try { return Promise.resolve(callback.apply(SD, args)) } catch (ex) { return Promise.reject(ex) } })) } } class ServiceContainer { #services = new Map() #singletons = new Map() constructor(...servicesParams) { servicesParams.forEach(this.register.bind(this)) } get services () { return this.#services } get singletons() { return this.#singletons } register(params) { if (ServiceContainer.isConstructor(params)) { if (typeof params.name !== 'string') { throw new Error('params.name is not a string.') } params = {name:params.name, definition:params} } if (typeof params !== 'object') { throw new Error('params is not an object.') } [ 'name', 'definition', ].forEach((key) => { if (!(key in params)) { console.error('Invalid service %o registration.', params) throw new Error(`params.${key} is not defined.`) } }) const opts = {definition: params.definition} if ('dependencies' in params) { if (Array.isArray(params.dependencies)) { params.dependencies.forEach((dep) => { if (typeof dep !== 'string') { throw new Error('dependency name is not a string.') } }) opts.dependencies = params.dependencies } else { throw new Error('params.dependencies is not an array.') } } if (params.singleton) { opts.singleton = true } this.#services.set(params.name, opts) return Object.assign({name: params.name}, opts) } get(name) { const ctorInfos = this.#services.get(name) if (!ctorInfos) { return } if(!ServiceContainer.isConstructor(ctorInfos.definition)) { return ctorInfos.definition } if(!ctorInfos.singleton) { return this._createInstance(ctorInfos) } const singletonInstance = this.#singletons.get(name) if(singletonInstance) { return singletonInstance } const newSingletonInstance = this._createInstance(ctorInfos) this.#singletons.set(name, newSingletonInstance) return newSingletonInstance } _getResolvedDependencies(service) { let classDependencies = [] if(service.dependencies) { classDependencies = service.dependencies.map(this.get.bind(this)) } return classDependencies } _createInstance(service) { if (!ServiceContainer.isClass(service.definition)) { // Call as normal function. return service.definition(...this._getResolvedDependencies(service)) } // Use new return new service.definition(...this._getResolvedDependencies(service)) } static isClass(definition) { return typeof definition === 'function' && Boolean(definition.prototype) && definition.prototype.constructor === definition } static isConstructor(definition) { return typeof definition === 'function' } } /** * * @param {string} tag * @param {object} attributes * @param {string | Array} classes * @param {string | Node | Array} * @returns {HTMLElement} */ function createElement(tagName, attributes, classes, textOrElements) { const element = document.createElement(tagName) if (attributes) { Object.entries(attributes).forEach(([key, value]) => { if (value !== undefined && value !== null) { element.setAttribute(key, value) } }); } if (classes) { (Array.isArray(classes) ? classes : [classes]).forEach(className => element.classList.add(className)) } if (textOrElements) { const children = Array.isArray(textOrElements) ? textOrElements : [textOrElements] children.forEach(textOrElem => { if (textOrElem instanceof Node) { element.appendChild(textOrElem) } else { element.appendChild(document.createTextNode(textOrElem)) } }) } return element } /* * Add a listener for arrays * @param {keyof Array} method * @param {(args) => {}} callback */ Array.prototype.addEventListener = function(method, callback) { const originalFunction = this[method] if (originalFunction) { this[method] = function() { originalFunction.apply(this, arguments) callback.apply(this, arguments) } } } /** * @typedef {object} TabOpenDetails * @property {HTMLElement} contentElement * @property {HTMLElement} labelElement * @property {number} timesOpened * @property {boolean} firstOpen */ /** * @typedef {object} CreateTabRequest * @property {string} id * @property {string | Node | (() => (string | Node))} label * Label text or an HTML element * @property {string} icon * @property {string | Node | Promise | (() => (string | Node | Promise)) | undefined} content * HTML string or HTML element * @property {((TabOpenDetails, Event) => (undefined | string | Node | Promise)) | undefined} onOpen * If an HTML string or HTML element is returned, then that will replace the tab content * @property {string | undefined} css */ /** * @param {CreateTabRequest} request */ function createTab(request) { if (!request?.id) { console.error('createTab() error - id is required', Error().stack) return } if (!request.label) { console.error('createTab() error - label is required', Error().stack) return } if (!request.icon) { console.error('createTab() error - icon is required', Error().stack) return } if (!request.content && !request.onOpen) { console.error('createTab() error - content or onOpen required', Error().stack) return } const tabsContainer = document.querySelector('.tab-container') if (!tabsContainer) { return } const tabsContentWrapper = document.querySelector('#tab-content-wrapper') if (!tabsContentWrapper) { return } console.debug('creating tab: ', request) if (request.css) { document.querySelector('body').insertAdjacentElement( 'beforeend', createElement('style', { id: `tab-${request.id}-css` }, undefined, request.css), ) } const label = typeof request.label === 'function' ? request.label() : request.label const labelElement = label instanceof Node ? label : createElement('span', undefined, undefined, label) const tab = createElement( 'span', { id: `tab-${request.id}`, 'data-times-opened': 0 }, ['tab'], createElement( 'span', undefined, undefined, [ createElement( 'i', { style: 'margin-right: 0.25em' }, ['fa-solid', `${request.icon.startsWith('fa-') ? '' : 'fa-'}${request.icon}`, 'icon'], ), labelElement, ], ) ) tabsContainer.insertAdjacentElement('beforeend', tab) const wrapper = createElement('div', { id: request.id }, ['tab-content-inner'], 'Loading..') const tabContent = createElement('div', { id: `tab-content-${request.id}` }, ['tab-content'], wrapper) tabsContentWrapper.insertAdjacentElement('beforeend', tabContent) linkTabContents(tab) function replaceContent(resultFactory) { if (resultFactory === undefined || resultFactory === null) { return } const result = typeof resultFactory === 'function' ? resultFactory() : resultFactory if (result instanceof Promise) { result.then(replaceContent) } else if (result instanceof Node) { wrapper.replaceChildren(result) } else { wrapper.innerHTML = result } } replaceContent(request.content) tab.addEventListener('click', (e) => { const timesOpened = +(tab.dataset.timesOpened || 0) + 1 tab.dataset.timesOpened = timesOpened if (request.onOpen) { const result = request.onOpen( { contentElement: wrapper, labelElement, timesOpened, firstOpen: timesOpened === 1, }, e, ) replaceContent(result) } }) }