cahir
Version:
flexible interface for method chaining using Proxy and tagged template literals
1,173 lines • 88.9 kB
JavaScript
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);
/*TODO: have to find an elegant way to