UNPKG

cahir

Version:

flexible interface for method chaining using Proxy and tagged template literals

1,173 lines 88.9 kB
import ch from "../../collections/DOM/ch.0.0.10.es.js"; const simpleUpload = ((symbols) => { "use strict"; const wk = new WeakMap(), noOp = () => {}, setShift = set => { const [firstItem] = set; set.delete(firstItem); return firstItem; }, setGetLast = set => [...set].at(-1), isNearBot = function(element, epsilon = 16){ return element.scrollHeight - element.scrollTop - element.clientHeight <= epsilon; }, /* TODO, backport this to https://github.com/IbrahimTanyalcin/Cahir/blob/master/collections/DOM/ DO NOT forget to uncomment 'this.lastOP = ' */ throttle_v2 = function(f, {thisArg = void(0), delay=100, defer=true} = {}){ const that = this, settle = (args) => { resolver?.(f.apply(thisArg, args)); resolver = prom = null; tmstmp = performance.now(); }; let timeout, prom, resolver, tmstmp = performance.now(); return /*this.lastOp = */function(...args) { clearTimeout(timeout); thisArg = thisArg ?? that; let elapsed = performance.now() - tmstmp; if (resolver) { if (defer || (!defer && elapsed < delay)) { timeout = setTimeout(() => { settle(args); }, delay); } else { settle(args); } return prom; } return prom = new Promise(res => { resolver = res; timeout = setTimeout(() => { settle(args); }, delay); }) } }, mimeImg = [/^image\/[\w+-]+$/i], mimeVid = [/^video\/[\w+-]+$/i, /^application\/vnd\.apple\.mpegurl$/i], isImg = (file) => mimeImg.some(r => r.test(file.type)), isVid = (file) => mimeVid.some(r => r.test(file.type)), isFile = (file) => !isImg(file) && !isVid(file), attachFile = function({event:e, values:v}){ const inp = ch[`input{ "prop": [["type", "file"], ["multiple", "true"]] }`]; ch(inp).on("change", function(){ const files = this.files; if(!files.length){return} createFileListIcons(files, v.upload); }); inp.click(); }, createFileListIcons = function(fileList, target){ for(const file of fileList){ createFileIcon(file, target); } }, createFileIcon = function(file, target){ const button = ch.dom`<button type="button" title="${file.name}" class="msger-file"></button>`; ch(button).set(symbols.file, file).on("click", function(){this.remove()}); switch (true) { case isImg(file): if(file.size <= 5242880) { const _url = URL.createObjectURL(file); ch(ch.img).satr("src", _url) .on("load", function(){ URL.revokeObjectURL(_url); }) .on("error", function(){ URL.revokeObjectURL(_url); this.remove(); button.innerHTML = `<i class="fa fa-warning"></i>`; button.title = "error preview" }) .appendTo(button); } else { button.innerHTML = `<i class="fa fa-image"></i>`; } break; case isVid(file): button.innerHTML = `<i class="fa fa-video-camera"></i>`; break; case isFile(file): default: button.innerHTML = `<i class="fa fa-file"></i>`; } target.appendChild(button); }, getInputContents = function(el){ const v = wk.get(el); //values //return [...v.input.children].map(d => d.textContent).join("\n"); //TODO grab the contents of the textarea and provide that return v.input.value; }, getAttachedFiles = function(el){ const v = wk.get(el); //values return [...v.upload.children].map(d => d[symbols.file]) }, _onsend = function(f, namespace = ""){ const v = wk.get(this), state = ch.state(), send = this[symbols.send]; if(!send){return} ch(send).on(`click@${namespace}`, () => this.send(f)).state(state); return this; }, _offsend = function(namespace = ""){ //if(!namespace){return} const v = wk.get(this), state = ch.state(), send = this[symbols.send]; if(!send){return} ch(send).off(`@${namespace}`).state(state); return this; }, _onready = function(f, ...args){ this.ready().then(() => f.apply(this, args)); return this; }, genHexStr = (function(){ const ceil = Math.ceil, log = Math.log, min = Math.min, rand = Math.random, log10b16 = log(10) / log(16), maxPow10 = Math.log(Number.MAX_SAFE_INTEGER) / Math.log(10) | 0; return function (complexity = 6, reps =2, prefix = "", postfix = "") { let padding = "0".repeat(ceil(complexity * log10b16)), ceiling = 10 ** min(maxPow10, complexity); return prefix + Array.from({length: reps}, d => ( padding + (+(rand() * ceiling).toFixed(0)).toString(16) ).slice(-padding.length)).join("").replace(/^0/,"f") + postfix } })(), messageTypes = { "info": { attrClass: "info-msg", attrFaClass: "fa-info-circle", }, "prompt": { attrClass: "prompt-msg", attrFaClass: "fa-question-circle", main_div: ` <bioinfo-input data-title="Enter a suitable file name" data-label="File name" data-fadein data-anycase data-colors="font,var(--base-font-color)" ></bioinfo-input> `, footer: `<button><i class="fa fa-check-circle"></i></button>` }, "warning": { attrClass: "warning-msg", attrFaClass: "fa-warning" }, "success": { attrClass: "success-msg", attrFaClass: "fa-thumbs-up" }, "error": { attrClass: "error-msg", attrFaClass: "fa-warning" }, "progress": { attrClass: "progress-msg", attrFaClass: "fa-cogs", main_div: ` <div data-value="000%" data-indefinite class="progress-bar" style="width: 100%; display: flex; position: relative;"> <div style="flex-grow: 1; overflow: hidden; width: auto; padding: 4px; background-color: rgba(0, 0, 0, 0.1); box-shadow: inset 0px 1px 2px rgba(0, 0, 0, 0.2); border-radius: 3px;"> <div style="width: 55%; transition: none; height: 16px; border-radius: 3px; background-size: 35px 35px; background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.125) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.125) 50%, rgba(255, 255, 255, 0.125) 75%, transparent 75%, transparent); display: block; float: left; box-shadow: inset 0px -1px 2px rgba(0, 0, 0, 0.1);"> </div> </div> </div> ` }, "confirm": { attrClass: "confirm-msg", attrFaClass: "fa-question-circle", footer: `<button><i class="fa fa-close"></i></button><button><i class="fa fa-check-circle"></i></button>` } }, _rgxAttr = /^\/(?<body>.+)\/(?<flags>[gimsuy]*)$/gi, _noOPRgx = new RegExp("", ""), parseRegexFromAttr = function(str){ if (!str) {return _noOPRgx} try { str = atob(str) } catch {} try { let [{groups:{body = "", flags = ""}} = {groups: {body: "", flags: ""}},] = [...str.matchAll(_rgxAttr)]; return new RegExp(body, flags.replace(/g/g,"")); } catch {} return _noOPRgx; }, parseJSONFromAttr = function(str){ if(!str) {return {}} try { str = atob(str) } catch {} try { return JSON.parse(str); } catch {} return {}; }, tauto = () => true, wait = (ms) => new Promise(r => setTimeout(r, ms)), textMimes = { "calendar": [".ics", ".ifb"], "css": [".css"], "csv": [".csv"], "html": [".html", ".html"], "javascript": [".js", ".cjs", ".mjs"], "markdown": [".md", ".markdown"], "plain": [".txt", ".text", ".log"], "sql": [".sql"], "xml": [".xml"], "x-asm": [".s", ".asm"], "x-batch": [".bat", ".cmd"], "x-bed": [".bed"], "x-c": [".c", ".h"], "x-c++": [".cpp", ".hpp", ".cc", ".cxx"], "x-config": [".conf", ".cfg"], "x-diff": [".diff", ".patch"], "x-dockerfile": [".dockerfile"], "x-fastq": [".fq", ".fastq"], "x-fasta": [".fa", ".fas", ".fasta"], "x-genbank": [".gb", ".gbk"], "x-gff": [".gff", ".gff3"], "x-go": [".go"], "x-java-source": [".java"], "x-java-properties": [".properties"], "x-latex": [".tex", ".ltx"], "x-log": [".log"], "x-lua": [".lua"], "x-makefile": [".mk"], "x-newick": [".nwk", ".newick"], "x-org" : [".org"], "x-perl": [".pl", ".pm"], "x-php": [".php", ".phtml"], "x-pdb": [".pdb"], "x-python": [".py"], "x-r": [".r", ".R"], "x-rustsrc": [".rs"], "x-sam": [".sam"], "x-scss": [".scss"], "x-sh": [".sh", ".bash", ".zsh"], "x-texinfo": [".texi", ".texinfo"], "x-toml": [".toml"], "tsv": [".tsv"], "vnd.graphviz": [".dot"], "x-vcf": [".vcf"], "x-yaml": [".yaml", ".yml"] }, textMimesMap = (() => { const mimeMap = new Map(); for (const [pmime, extArr] of Object.entries(textMimes)){ for (const ext of extArr) { mimeMap.set(ext, `text/${pmime}`) } } return mimeMap; })(), parseFilename = function (name){ var match = name.match(/([^\/\\]+?)(\.[^.]*)?$/i); return { base: match?.[1] ?? (()=>{throw new Error("cannot parse filename")})(), ext: match?.[2] ?? "", get full() { delete this.full; return this.full = this.base + this.ext } }; }, getMimeFromFilename = function(filename, v = {}){ const parsed = parseFilename(filename); return v?.overrideMime || textMimesMap.get(parsed.ext) || "text/plain"; }, encode = TextEncoder.prototype.encode.bind(new TextEncoder), asyncStreamGen = async function* (ctrl, strs, chunkSize, options) { //console.log("stream generating", Date.now()); const progress = options?.element?.[symbols.issueMessage]?.({msg:`Converting ${options.filename}`, fadeout: 20000, type: "progress"}), delay = options?.values?.streamEnqueueDelay; /** * Why was I conservative to divide chunkSize by 4? Isnt it the other way around? * slice does not cut by char, it cuts by byte. */ let sliceSize = chunkSize / 4 | 0; //max 4 bytes per char let totalStrsLen = 0; for (let str of strs) { totalStrsLen += str.length; } let offset = 0; OUTER: for (let str of strs) { let len = str.length; for (let i = 0, j, chunk, lastChar, nextChar; i < len; i += sliceSize) { delay && await wait(delay); chunk = str.slice(i, i + sliceSize); if(!chunk.length){continue OUTER} lastChar = chunk.slice(-1); j = i + chunk.length; nextChar = str[j]; if (lastChar >= "\uD800" && lastChar <= "\uD8FF"){ if (j < len && nextChar >= "\uDC00" && nextChar <= "\uDFFF"){ chunk += nextChar; ++i; } } //console.log("stream enquing", Date.now(), "offset" ,offset, "i", i, "chunk len", chunk.length, "total", totalStrsLen); progress?.set((offset + i + chunk.length) / totalStrsLen); ctrl.enqueue(encode(chunk)); yield } offset += len; } //progress?.rm(); progress?.msg("Done! You can close this message."); ctrl.close(); }, stringsToUint8 = async function(options, ...strs){ let offset, chunks, chunkSize = 64 * 1024, //if you want to force char by char streaming, set this to 4 totalByteLength, concatUint8, asyncIt; const stream = new ReadableStream({ start (ctrl) { totalByteLength = 0; asyncIt = asyncStreamGen(ctrl, strs, chunkSize, options); }, pull (ctrl) { //console.log("stream pulling!", Date.now()); return asyncIt.next() }, cancel () {} }, {highWaterMark: 3}); chunks = []; for await (const chunk of stream) { chunks.push(chunk); totalByteLength += chunk.length; } concatUint8 = new Uint8Array(totalByteLength); offset = 0; for (const chunk of chunks){ concatUint8.set(chunk, offset); offset += chunk.length; } /*console.log("UINT8:", concatUint8); console.log("decoded:", new TextDecoder().decode(concatUint8)); console.log("buffer:", concatUint8.buffer);*/ return concatUint8; }, bufferToFile = function({buffer, name, type}){ return new File([buffer], name, {type}) }, pasteOrBeforeinput = new Set(["paste", "beforeinput"]), handleTextarea = async function ({event, values:v, element:el}){ //TODO increment and decrement at finally step of trty catch //a counter. event.preventDefault?.(); if (v.textareaDisabled) { return el[symbols.issueMessage]({msg:"Text input is currently disabled", fadeout: 5000, type: "info"}); } let currVal = this.value, selStart = this.selectionStart, selEnd = this.selectionEnd, selDiff = selEnd - selStart, totalLen, data = ""; switch (true) { case event.type === "paste": data = (event.clipboardData || window.clipboardData).getData("text"); totalLen = currVal.length - selDiff + data.length; break; case event.type === "beforeinput": data = event.data ?? ""; totalLen = currVal.length - selDiff + data.length; break; case event.type === "input": data = ""; totalLen = currVal.length; } if (totalLen > v.textareaMaxSize){ this.readOnly = true; try { v[symbols.textAreaStreamBusy] ||= 0; ++v[symbols.textAreaStreamBusy]; const filename = await el[symbols.issueMessage]({ msg: `The text exceeds allowed length of ${v.textareaMaxSize}.<br>` + `Provide a filename for it to be saved as a file instead.`, fadeout: 60000, type: "prompt" }), type = getMimeFromFilename(filename, v), _file = bufferToFile({ /**await has lowever precedence than prop access*/ buffer: (await stringsToUint8({filename, element: el, values: v}, currVal.slice(0, selStart), data, currVal.slice(selEnd))).buffer, name: filename, type }); createFileIcon(_file, v.upload); //console.log("mime is", type); //console.log("capturing:", currVal.slice(0, selStart) + data + currVal.slice(selEnd)); this.value = ""; } catch (err) { el[symbols.issueMessage]({ msg:`There was an error during file conversion.<br>` + `reason: ${err?.message || err || "unknown"}<br>` + `your message is truncated to max allowed length.`, fadeout: 5000, type: "error" }); this.value = currVal.slice(0, v.textareaMaxSize); } finally { --v[symbols.textAreaStreamBusy]; return this.readOnly = false; } } if(!pasteOrBeforeinput.has(event.type)){return} this.value = currVal.slice(0, selStart) + data + currVal.slice(selEnd); this.selectionStart = this.selectionEnd = selStart + data.length; }, refreshFormAction = function({values: v, element: el}){ if (el.hasAttribute("data-form-action")){ const state = ch.state(); ch(el); const currPath = ch.gatr("data-form-action"), currMethod = (el.hasAttribute("data-form-method") ? ch.gatr("data-form-method") : "POST").toUpperCase(); let currReqInit = el.hasAttribute("data-form-request-init") ? ch.gatr("data-form-request-init") : void(0); if ( v?.formAction === currPath && v?.formMethod === currMethod && v?.formReqInit === currReqInit ) { ch.state(state); return } currReqInit = parseJSONFromAttr(currReqInit); currReqInit.method = currMethod; if(v?.formAction){el.offsend("data-form-action")} v.formAction = function({files, text, abort, progress}){ const fData = new FormData(), signal = abort.signal; let fileCount = 0, totalFileSizeInBytes = 0, textSizeInBytes = 0, totalSizeInBytes = 0; for (const file of files){ fileCount++; totalFileSizeInBytes += file.size; fData.append("files[]", file); } textSizeInBytes += encode(text).length; totalSizeInBytes += totalFileSizeInBytes + textSizeInBytes; fData.append("text", text); fData.append("fileCount", fileCount); fData.append("totalFileSizeInBytes", totalFileSizeInBytes); fData.append("textSizeInBytes", textSizeInBytes); fData.append("totalSizeInBytes", totalSizeInBytes); currReqInit.body = fData; currReqInit.signal = signal; return fetch(currPath, currReqInit); } el.onsend(v.formAction, "data-form-action"); ch.state(state); } else { if (v?.formAction) { el.offsend("data-form-action"); v.formAction = void(0); } } }; return function simpleUpload({name, attrs, styles, props, data, el, proto}) { if (!el[symbols.initialized]){ proto[symbols.initialized] = true; proto[symbols.nuf] = [null, void(0), false]; proto[symbols.nue] = [null, void(0), ""]; proto.remove = function(){ return ch(this).animate([{ transform: "translate(0%, -100%)", opacity: 0 }], {duration:1000, easing: "ease-in-out", fill: "both"}).lastOp.then(() => (this.parentNode.removeChild(this), this)); } proto.color = function(f, v) { if (f instanceof Array){ f.forEach(([f,v]) => this.color(f, v)) return this; } const colors = Object.fromEntries( (ch(this).gatr("data-colors") ?? "") .split(",") .filter(Boolean) .reduce((ac,d,i,a) => { if(!(i % 2)){ ac.push([d, a[i+1]]) } return ac; },[])); colors[f] = v; return ch.satr("data-colors", `${Object.entries(colors).flat()}`).selected; } proto.css = function (str, pos) { const shadow = this.shadowRoot, style = shadow.querySelector("style[data-for=simple-upload]"), sheet = style.sheet; pos = pos ?? sheet.cssRules.length; sheet.insertRule(str, pos); return this; } proto.enable = proto.play = function() { this.removeAttribute("data-disabled"); return this; } proto.disable = proto.pause = function() { this.setAttribute("data-disabled", ""); return this; } proto.setCancellable = function(bool){ if(this[symbols.nue].includes(bool) || !!bool){ this.setAttribute("data-cancellable", ""); return this; } this.removeAttribute("data-cancellable"); return this; } proto.hideHeader = function() { this.setAttribute("data-hide-header", ""); return this; } proto.showHeader = function() { this.removeAttribute("data-hide-header"); return this; } proto.hideTextarea = function() { this.setAttribute("data-hide-textarea", ""); return this; } proto.showTextarea = function() { this.removeAttribute("data-hide-textarea"); return this; } proto.disableTextarea = function() { this.setAttribute("data-disable-textarea", ""); return this; } proto.enableTextarea = function(){ this.removeAttribute("data-disable-textarea"); return this; } proto.disableDragndrop = function(){ this.setAttribute("data-disable-dragndrop", ""); return this; } proto.enableDragndrop = function(){ this.removeAttribute("data-disable-dragndrop"); return this; } proto.showTyping = function(ms){ this.setAttribute("data-typing", ms); return this; } proto.hideTyping = function(){ this.removeAttribute("data-typing"); return this; } proto.setHeaderTitle = function(title){ this.setAttribute("data-title", title ?? ""); return this; } proto.setPlaceholder = function(ph){this.setAttribute("data-placeholder", ph ?? ""); return this;} proto.setOverrideMime = function(mime){ if(!mime){ this.removeAttribute("data-override-mime"); } else { this.setAttribute("data-override-mime", mime); } return this; } proto.setFilenameMessage = function(msg){this.setAttribute("data-filename-message", msg ?? "invalid filename"); return this;} proto.setFilenameValidator = function(f){ if(typeof f !== "function"){return this} const v = wk.get(this); //values v.validateFilename = f; return this; }; proto.setTextareaMessage = function(msg){this.setAttribute("data-textarea-message", msg ?? "invalid text"); return this;} proto.setTextareaValidator = function(f){ if(typeof f !== "function"){return this} const v = wk.get(this); //values v.validateTextarea = f; return this; }; proto.setTextareaMaxSize = function(len){len = len | 0 || 65536; this.setAttribute("data-textarea-max-size", len); return this;} proto.setSendingMessage = function(msg){this.setAttribute("data-sending-message", msg ?? "Sending..."); return this;} proto.setSentMessage = function(msg){this.setAttribute("data-sent-message", msg ?? "Done!"); return this;} proto.setConfirmingMessage = function(msg){this.setAttribute("data-confirming-message", msg ?? "Are you sure you want to cancel?"); return this;} proto.setConfirmedMessage = function(msg){this.setAttribute("data-confirmed-message", msg ?? "Canceled by user"); return this;} proto.setStreamEnqueueDelay = function(dly){ dly = Math.min(1000, Math.max(0, +dly | 0)); this.setAttribute("data-stream-enqueue-delay", dly); return this }; proto.setMessageOpacity = function(opacity){this.setAttribute("data-message-opacity", opacity ?? 0.85); return this;} proto.disableFile = function(){ this.setAttribute("data-file", "false"); return this; } proto.enableFile = function(){ this.removeAttribute("data-file"); return this; } proto.changeBackground = function(bg) { const v = wk.get(this); //values switch (true) { case typeof bg === "string": v.svg.parentNode.replaceChild(ch.dom`${bg}`, v.svg); break; case bg?.nodeType === 1: v.svg.parentNode.replaceChild(bg, v.svg); break; default: throw new Error("background can be a DOM string or an svg node"); } return this; } proto.clear = function() { if(!this[symbols.input]){return} this[symbols.input].value = ""; return this; } proto.clearFiles = function(){ const v = wk.get(this); //values [...v.upload.children].forEach(button => button.click()); return this; } proto.abortSend = function(msg){ const v = wk.get(this); //values msg ||= "Canceled by the app"; v[symbols.abort]?.abort(msg); return this; } proto[symbols.upAttrs] = async function({values:v, el}){ ch(el); v.colors = el.colors = Object.fromEntries( (ch.gatr("data-colors") ?? "") .split(",") .filter(Boolean) .reduce((ac,d,i,a) => { if(!(i % 2)){ ac.push([d, a[i+1]]) } return ac; },[])); v.border = ch.gatr("data-border"); v.borderRadius = ch.gatr("data-border-radius"); v.backgroundOpacity = ch.gatr("data-background-opacity"); v.boxShadow = ch.gatr("data-box-shadow"); v.headingBackgroundOpacity = ch.gatr("data-heading-background-opacity"); v.widthRatio = ch.gatr("data-width-ratio"); v.aspectRatio = ch.gatr("data-aspect-ratio"); v.margin = ch.gatr("data-margin"); v.padding = ch.gatr("data-padding"); v.fadein = el.fadein = (ch.gatr("data-fadein") ?? null) !== null ? 1 : 0; v.disabled = el.hasAttribute("data-disabled"); //TODO: add to ch 'hatr' v.cancellable = el.hasAttribute("data-cancellable"); v.headerHidden = el.hasAttribute("data-hide-header"); v.historyLength = +ch.gatr("data-history-length") || 100; [v.typingMs = 0, v.typingName = "anonymous"] = `${ch.gatr("data-typing") || ""}`.split(",").filter(Boolean); v.typingMs = +v.typingMs || 0; v.headerTitle = ch.gatr("data-title"); v.fileDisabled = (ch.gatr("data-file") ?? "").toLowerCase() === "false"; v.placeholder = ch.gatr("data-placeholder") ?? ""; v.textareaHidden = el.hasAttribute("data-hide-textarea"); v.textareaRegex = el.hasAttribute("data-textarea-regex") ? parseRegexFromAttr(ch.gatr("data-textarea-regex")) : _noOPRgx; v.filenameRegex = el.hasAttribute("data-filename-regex") ? parseRegexFromAttr(ch.gatr("data-filename-regex")) : _noOPRgx; v.textareaDisabled = el.hasAttribute("data-disable-textarea"); v.dragndropDisabled = el.hasAttribute("data-disable-dragndrop"); v.messageOpacity = ch.gatr("data-message-opacity") ?? 0.85; v.filenameMessage = ch.gatr("data-filename-message") ?? "invalid filename"; v.textareaMessage = ch.gatr("data-textarea-message") ?? "invalid text"; v.sendingMessage = ch.gatr("data-sending-message") ?? "Sending..."; v.sentMessage = ch.gatr("data-sent-message") ?? "Done!"; v.confirmingMessage = ch.gatr("data-confirming-message") ?? "Are you sure you want to cancel?"; v.confirmedMessage = ch.gatr("data-confirmed-message") ?? "Canceled by user"; v.validateFilename = v.validateFilename || tauto; v.validateTextarea = v.validateTextarea || tauto; v.textareaMaxSize = ch.gatr("data-textarea-max-size") | 0 || 65536; v.overrideMime = ch.gatr("data-override-mime") ?? ""; v.streamEnqueueDelay = Math.min(1000, Math.max(0, +ch.gatr("data-stream-enqueue-delay") | 0)); refreshFormAction({values: v, element: el}); return v; }; /* TODO extend below to accept array selectors so that nested cssRules can be styled */ proto[symbols.upStyle] = function({values:v, el, selector, prop, val}){ //console.log("upStyle", el); selector = selector.trim(); el[symbols.stylesheet] = el[symbols.stylesheet] || el?.shadowRoot?.styleSheets?.[0]; if (!el[symbols.stylesheet]){return proto[symbols.upStyle]} for (const {style, selectorText} of el[symbols.stylesheet].cssRules){ if (selectorText?.trim() !== selector){continue} if (el[symbols.nuf].includes(val)) { style.removeProperty(prop); } else { style.setProperty(prop, val); } break; } return proto[symbols.upStyle]; } /* TODO cleanup unused ones and update the list to include nested styles */ proto[symbols.upStyles] = function({values:v, el, state}){ //console.log("upStyles", el, state); el[symbols.upStyle] ({values:v, el, selector: ":host", prop: "color", val:`${v.colors.font || "var(--font-color, DarkSlateGray)"}`}) ({values:v, el, selector: ":host", prop: "background-color", val:`${v.colors.background || "var(--bg-color, #f9f9f9)"}`}) ({values:v, el, selector: ":host", prop: "border-radius", val:`${v.borderRadius || "var(--border-radius, 4px)"}`}) ({values:v, el, selector: ":host", prop: "animation", val:`${ v.fadein ? "animation: fadein-translate-y 0.4s ease-in-out 0s 1 normal forwards running;" : false }`}) ({values:v, el, selector: ":host", prop: "--state", val:`${state}`}) ({values:v, el, selector: ":host", prop: "transform", val:`translate(0px, ${(1 - state) * -100}px)`}) ({values:v, el, selector: ":host", prop: "opacity", val:`${state}`}) ({values:v, el, selector: "pattern > g", prop: "stroke", val:`${v.colors.stroke || "none"}`}) ({values:v, el, selector: "pattern > g", prop: "fill", val:`hsl(from ${v.colors.fill || v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 15) / alpha)`}) ({values:v, el, selector: ".msger-header", prop: "display", val:`${v.headerHidden ? "none" : "flex"}`}) ({values:v, el, selector: "#typing-indicator", prop: "visibility", val:`${v.typingMs ? "visible" : "hidden"}`}) ({values:v, el, selector: ".msger-chat", prop: "display", val:`${v.textareaHidden ? "none" : "revert"}`}) ({values:v, el, selector: ".msger", prop: "height", val:`${v.textareaHidden ? "calc((var(--currHeight) - var(--currChatHeight)) * 1px)" : "calc(var(--currHeight) * 1px)"}`}) ({values:v, el, selector: ".info-msg, .success-msg, .warning-msg, .error-msg, .prompt-msg, .progress-msg, .confirm-msg", prop: "--opacity", val:`${v.messageOpacity}`}) }; proto[symbols.upSvg] = async function({values:v, el}){ //console.log("upSvg", el); if(!v.svg){return} //svg updates here v.calcResize?.(); }; proto[symbols.upHTML] = async function({values:v, el}){ el[symbols.send] = el[symbols.send] || v.section.querySelector(".msger-send-btn"); el[symbols.input] = el[symbols.input] || v.input; el[symbols.typing] = el[symbols.typing] || v.main.querySelector("#typing-indicator"); el[symbols.header] = el[symbols.header] || el[symbols.shadow].querySelector("header"); el[symbols.headerTitle] = el[symbols.headerTitle] || el[symbols.header]?.querySelector(".msger-header-title")?.lastChild; if(v.disabled){ v.input.readOnly = true; el[symbols.send].innerHTML = `<i class="fa fa-stop"></i>`; el[symbols.send].disabled = true; } else { v.input.readOnly = false; el[symbols.send].innerHTML = `<i class="fa fa-send"></i>` el[symbols.send].disabled = false; } if(v.cancellable && !v.disabled) { el[symbols.send].innerHTML = `<i class="fa fa-close"></i>` } if(v.fileDisabled) { v.attach.disabled = true; } else { v.attach.disabled = false; } /**Below is commented to allow updating immediately */ if (/*!v[symbols.typing] &&*/ v.typingMs) { el[symbols.typing].firstElementChild.textContent = v.typingName; if (isFinite(v.typingMs)) { clearTimeout(v?.[symbols.typing]); v[symbols.typing] = setTimeout(() => { el.removeAttribute("data-typing"); v[symbols.typing] = void(0); }, v.typingMs); } } if (v.headerTitle) { el[symbols.headerTitle].textContent = ` ${v.headerTitle} `; } if (v.placeholder) { v.textarea.placeholder = v.placeholder; } if (v.textareaDisabled) { el.clear(); v.textarea.readOnly = true; if(!v.placeholder){ v.textarea.placeholder = v.dragndropDisabled ? " " : "drag&drop"; } } else if (!v.placeholder) { v.textarea.placeholder = v.dragndropDisabled ? "copy/paste" : "copy/paste or drag&drop"; } }; proto.update = async function({values, el, state}, mList, obs){ //console.log("update", el); //prevent recursion if (mList.every(d => d.attributeName === "style")) { return; } LOOP: for (const mutRec of mList) { //console.log(mList); switch (`${mutRec.type}-${mutRec?.target?.assignedSlot ? "slot" : "host"}` ) { case "childList-host": //console.log("childList-host"); //update child somehow break; case "attributes-host": //console.log("attributes-host"); await el[symbols.upAttrs]({values, el}); await el[symbols.upStyles]({values, el, state}); await el[symbols.upSvg]({values, el}); await el[symbols.upHTML]({values, el}); break; case "characterData-host": //console.log("characterData-host"); break; case "childList-slot": //console.log("childList-slot"); break; case "attributes-slot": //console.log("attributes-slot"); break; case "characterData-slot": //console.log("characterData-slot"); break; default: continue LOOP; } } }; proto.send = async function(f){ const v = wk.get(this); if (v[symbols.busy]) { const prom = this[symbols.issueMessage]({msg: v.confirmingMessage, fadeout: 10000, type: "confirm"}); prom.catch(noOp).then(choice => choice && v[symbols.abort]?.abort(v.confirmedMessage)); return; } if(v[symbols.textAreaStreamBusy] > 0) { this[symbols.issueMessage]({msg: "Streaming busy. Try again later.", fadeout: 10000, type: "error"}); return; } const files = getAttachedFiles(this), textContent = getInputContents(this); if (!textContent && !files.length){ this[symbols.issueMessage]({msg: "No files or text", fadeout: 10000, type: "warning"}); return; } let payloadDetails = ""; if (files.length > 1) { payloadDetails += files.length + " files" } else if (files.length) { payloadDetails += "1 file" } if (textContent) { if (payloadDetails) { payloadDetails += " and text"; } else { payloadDetails += "text"; } } if(!v.textareaDisabled && (!v.textareaRegex.test(v.textarea.value) || !v.validateTextarea(v.textarea.value))) { this[symbols.issueMessage]({msg: v.textareaMessage, fadeout: 10000, type: "error"}); return; } v[symbols.busy] = true; v[symbols.abort] = new AbortController; let r,j; const signal = v[symbols.abort].signal, prom = new Promise((_r,_j) => [r, j] = [_r, _j]); signal.addEventListener("abort", function(){ j(signal.reason || "upload canceled"); }); const progress = this[symbols.issueMessage]({msg: `${v.sendingMessage}(${payloadDetails})`, fadeout: Infinity, type: "progress"}) try { this.setCancellable(); const result = await Promise.race([ prom, f.call(this, {files, textContent, text: textContent, abort: v[symbols.abort], progress}) ]); r(result); progress.set(1).freeze().msg(`Done!(${payloadDetails})`); this[symbols.issueMessage]({msg: `${v.sentMessage}(${payloadDetails})`, fadeout: 10000, type: "success"}); this.clear(); this.clearFiles(); } catch (err) { j(err?.message || err || "unknown"); this[symbols.issueMessage]({ msg: `There was an error during upload.<br>` + `reason: ${err?.message || err || "unknown"}`, fadeout: 10000, type: "error" }); progress.set(0).freeze().msg(`Failed!(${payloadDetails})`); } finally { v[symbols.busy] = false; v[symbols.abort] = void(0); this.setCancellable(false); wait(5000).then(() => progress.rm()); } return this; }; proto[symbols.issueMessage] = proto.issueMessage = function({ msg = "", fadeout = 5000, type = "info"} = {msg: "", fadeout: 5000, type: "info"}){ const v = wk.get(this); //values //I should consider adding v.self = el in component initialization instead of below const _el = this; const rndID = genHexStr(8, 2, "file-upload-close-button-"); let _abort = void(0); ch(v.overlay)` *> ${"div"} |> sappend ${0} addClass ${[messageTypes?.[type]?.attrClass ?? "info-mgs", "animated", "fadeInLeft"]} >> innerHTML ${` <header> <i class="fa ${messageTypes?.[type]?.attrFaClass ?? "fa-info-circle"}"></i> <button data-random-id='${rndID}'> <i class="fa fa-close"></i> </button> </header> <main> <span>${msg || ""}</span> <span></span> <div>${messageTypes?.[type]?.main_div ?? ""}</div> </main> <footer>${messageTypes?.[type]?.footer ?? ""}</footer> `} => ${() => () => { //switched from inline onclick due to CSP ch.select(`[data-random-id=${rndID}]`, v.overlay).on("click", function(e){ const that = this.parentElement.parentElement; ch(that) .addClass("fadeOutLeft") .animate([],{duration:1000}) .pipe("await", () => { ch(that.parentElement).rm([that]); }); }) }} => ${() => () => setTimeout( () => ch .exec(() => {_abort && _abort.abort("filename timedout")}) .select(`[data-random-id=${rndID}]`, v.overlay) ?.selected ?.click(), Math.min(fadeout, 0x7FFFFFFF)) } => ${() => async () => { await wait(17); _el[symbols.scrollToBottom](v.overlay); }}` switch (type) { case "prompt": { let res, rej; _abort = new AbortController(); const signal = _abort.signal, prom = new Promise((r,j) => [res, rej] = [r, j]), promptDiv = v.overlay.querySelector(`div:has([data-random-id=${rndID}])`), cancelButton = ch.select("button:has(.fa.fa-close)", promptDiv).selected, checkButton = ch.select("button:has(.fa.fa-check-circle)", promptDiv).selected, input = ch.select("bioinfo-input", promptDiv).selected, errorMessage = ch.select("main span:last-of-type", promptDiv).selected; signal.addEventListener("abort", function(){ rej(signal.reason || "filename aborted"); }); ch(input.shadowRoot.querySelector("input")).on("input",throttle_v2(function(){ if(this.textContent){this.textContent = ""} },{thisArg:errorMessage, defer: false, delay: 250})) (cancelButton).on("click@abort", function(e){ _abort.abort("filename canceled"); })(checkButton).on("click", function(e){ const inputValue = input.value(); if (!v.filenameRegex.test(inputValue) || !v.validateFilename(inputValue)){ return errorMessage.textContent = v.filenameMessage; } ch(cancelButton).off("click@abort").selected.click(); res(inputValue); }); return prom; } case "progress": { const promptDiv = v.overlay.querySelector(`div:has([data-random-id=${rndID}])`), cancelButton = ch.select("button:has(.fa.fa-close)", promptDiv).selected, progress = ch.select("main > div:last-of-type > div", promptDiv).selected, progressBar = progress.firstElementChild.firstElementChild, message = ch.select("main span:last-of-type", promptDiv).selected; return { value: function(x){ return this.set(x); }, set: function(x) { x = +x; if(x < 0 || isNaN(x) || !isFinite(x)){ return progress.setAttribute("data-indefinite",""); } const width = Math.min(100, Math.max(0, (x * 100 | 0))) + "%"; progress.removeAttribute("data-indefinite"); progress.setAttribute("data-value", ("000" + width).slice(-4)); progressBar.style.width = width; return this; }, remove: function(){ return this.rm(); }, rm: function(){ cancelButton.click(); return; }, message: function(msg){ return this.msg(msg); }, msg: function(msg){ message.textContent = msg; return this; }, freeze: function(){ this.set = () => this; return this; } } } case "confirm": { let r, j; const promptDiv = v.overlay.querySelector(`div:has([data-random-id=${rndID}])`), [cancelButton, cancelButton2] = promptDiv.querySelectorAll("button:has(.fa.fa-close)"), checkButton = ch.select("button:has(.fa.fa-check-circle)", promptDiv).selected, prom = new Promise((_r,_j) => [r, j] = [_r, _j]); ch(cancelButton2).on("click", (e) => cancelButton.click()) (cancelButton).on("click@reject", (e) => j(false)) (checkButton).on("click", (e) => {ch(cancelButton).off("click@reject").selected.click(); r(true);}); return prom; } default: return } } Object.defineProperties(proto, { onsend: { get: function(){ return _onsend }, set: function(f){ this.onsend(f) }, configurable: false }, offsend: { get: function(){ return _offsend}, set: function(namespace){ this.offsend(namespace)}, configurable: false }, onready: { get: function(){return _onready }, set: function(f){ this.onready(f)}, configurable: false } }); proto.ready = function(){ const v = wk.get(this); //values return v.readyPromise; } } const shadow = el[symbols.shadow] = el.attachShadow({ mode: "open" }); el[symbols.scrollToBottom] = throttle_v2(function(el){ el.scrollTo({ top: el.scrollHeight, behavior: 'smooth', }); }, {delay: 50}); let state = 0, toggle, calcResize; ch(el)` => ${({values:v}) => () => { const updateDelay = +ch.gatr("data-update-delay") || 50, resizeDelay = +ch.gatr("data-resize-delay") || 500, toggleDelay = +ch.gatr("data-toggle-delay") || 50; v.resizeDelay = resizeDelay; v.toggleDelay = toggleDelay; el[symbols.upAttrs] = ch.throttle(el[symbols.upAttrs], {delay: updateDelay}); el[symbols.upStyles] = ch.throttle(el[symbols.upStyles], {delay: updateDelay}); el[symbols.upSvg] = ch.throttle(el[symbols.upSvg], {delay: updateDelay}); /*ready mechanism*/ v.readyPromise = new Promise(r => v.ready = r); }} => ${({values}) => async () => { if (!wk.has(el)){wk.set(el, values)} return el[symbols.upAttrs]({values, el}) }} |> await ${({values:v}) => async () => { v.mutObs = el.mutObs ?? new MutationObserver(el.update.bind(el, {values:v, el, get state(){return state}})) }} |> await ${({values:v}) => async () => { calcResize = v.calcResize = ch.throttle(function(){ const parent = el?.parentElement; el[symbols.header] = el[symbols.header] || shadow.querySelector("header"); el[symbols.main] = el[symbols.main] || shadow.querySelector("main"); el[symbols.form] = el[symbols.form] || shadow.querySelector("form"); //if(!(parent && el[symbols.header] && el[symbols.form])){return} if(!parent){return} if(el[symbols.main]){el[symbols.main]._clH = el[symbols.main].clientHeight} const scrollBarWidth = parent.offsetWidth - parent.clientWidth, scrollBarHeight = parent.offsetHeight - parent.clientHeight, {width: w, height: h} = el?.parentElement?.getBoundingClientRect(); ch(el).style("--currWidth", (w - scrollBarWidth) * (+v.widthRatio || 0.95)) .style("--currHeight", (h - scrollBarHeight)) .style("--currChatHeight", (h - scrollBarHeight - (el[symbols.header]?.offsetHeight ?? 40) - (el[symbols.form]?.offsetHeight ?? 60)) | 0); }, {delay: v.resizeDelay ?? 500}); (v.robserver = new ResizeObserver(calcResize)).observe(el?.parentElement, {box: "border-box"}); }} |> await ${({values:v}) => async () => calcResize()} |> await ${({values:v}) => async () => { ch(shadow)` +> ${ch.dom` <style data-for="simple-upload"> *, *:after, *:before { box-sizing: border-box; } :host { box-sizing: border-box; width: calc(var(--currWidth) * 1px); margin: auto; //margin: 25px 10px; color: ${v.colors.font || "var(--font-color, DarkSlateGray)"}; background-color: ${v.colors.background || "var(--bg-color, #f9f9f9)"}; border: ${v.border || " var(--border, 2px solid #ddd)"} border-radius: ${v.borderRadius || "var(--border-radius, 4px)"}; display: flex; flex-flow: column wrap; justify-content: space-between; position: relative; font-size: 1rem; font-family: Helvetica, sans-serif; container: component-container; container-type: inline-size; ${ v.fadein ? "animation: fadein-translate-y 0.4s ease-in-out 0s 1 normal forwards running;" : "" } --state: ${state}; transform: translate(0px, ${(1 - state) * -100}px); opacity: ${state}; box-shadow: ${v.boxShadow || "var(--box-shadow, 0 15px 15px -5px rgba(0, 0, 0, 0.2))"}; --color-auto-invert: var(--font-color, color(from var(--bg-color, #f9f9f9) xyz clamp(0.058, calc(1 - 3 * x * x + 2 * x * x * x), 0.876) clamp(0.076, calc(1 - 3 * y * y + 2 * y * y * y), 0.950) clamp(0.089, calc(1 - 3 * z * z + 2 * z * z * z), 1.035) / alpha)); } .msger { width: calc(var(--currWidth) * 1px); //height: auto; height: calc(var(--currHeight) * 1px); position: relative; display: flex; flex-direction: column; } .msger-header { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; padding: ${v.padding || "var(--padding, 10px)"}; border-bottom: ${v.border || " var(--border, 2px solid #ddd)"}; background: #eeeeee; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 10) / alpha); color: SlateGray; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 10) / alpha); /*TODO: have to find an elegant way to combine above v.colors.font with below*/ color: var(--color-auto-invert); & button { padding: 0; height: auto; aspect-ratio: 1 / 1; width: 2rem; display: flex; align-items: center; justify-content: center; background: #eeeeee; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 10) / alpha); color: SlateGray; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 10) / alpha