UNPKG

cahir

Version:

flexible interface for method chaining using Proxy and tagged template literals

1,115 lines (1,108 loc) 59.5 kB
const simpleChat = ((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); }) } }, scrollToBottom = throttle_v2(function(el){ el.scrollTo({ top: el.scrollHeight, behavior: 'smooth', }); }, {delay: 50}), defDangerousParser = ({current, incoming, parent, bubble, chat, component}) => { ch(parent)`+> ${ch.dom`${incoming}`} -> ${bubble} +< ${chat}` }, defParser = ({current, incoming, parent, bubble, chat, component}) => { ch(parent)`+-> ${ch.span} >> textContent ${incoming} style word-break ${"break-word"} -> ${bubble} +< ${chat}` }, 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} for(const file of files){ 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>`; } v.upload.appendChild(button); } }); inp.click(); }, getInputContents = function(el){ const v = wk.get(el); //values return [...v.input.children].map(d => d.textContent).join("\n"); }, 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; }; return function simpleChat({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-chat]"), 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.hideHeader = function() { this.setAttribute("data-hide-header", ""); return this; } proto.showHeader = function() { this.removeAttribute("data-hide-header"); 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.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.defParser = defParser; proto.defDangerousParser = defDangerousParser; proto.addBubble = async function({ side = "left" , avatar = null, name = "anonymous", time = null, content = "", parser = defParser, cb = noOp }) { const values = wk.get(this); if(!values){return} let _return, prev = values.chat[symbols.busy]; let r,j; values.chat[symbols.busy] = new Promise((_r, _j) => {[r, j] = [_r, _j]}) try { prev = await prev; } catch (err) { prev = err } if(!time){ time = new Date(); time = `${time.getHours()}:${time.getMinutes()}` } switch (true) { case !avatar: avatar = ch.dom`<i class="fa fa-user"></i>` break; case typeof avatar === "string": avatar = ch.dom`${avatar}`; break; case avatar?.nodeType === 1: break; default: //throw new Error("avatar can only be a string or element"); j(_return = new Error("avatar can only be a string or element")); return _return; } const node = ch.dom` <div class="msg ${side}-msg"> <div class="msg-img"></div> <div class="msg-bubble"> <div class="msg-info"> <div class="msg-info-name">${name}</div> <div class="msg-info-time">${time}</div> </div> <div class="msg-text"></div> </div> </div>`, msgText = node.querySelector(".msg-text"); ch(node)`>> ...${[symbols.raw, ""]} @> ${".msg-img"} +> ${avatar}` const hSet = (values[symbols.history] = values[symbols.history] || new Set()).add(node); while(hSet.size > values.historyLength){setShift(hSet).remove()} try { const retVal = await parser({current: "", incoming: content, parent: msgText, bubble: node, chat: values.chat, prev, component: this}), next = await cb({current: "", incoming: content, parent: msgText, bubble: node, chat: values.chat, prev, component: this, retVal}); node[symbols.raw] += content; r(_return = next); } catch (err) { j(_return = err) } if (isNearBot(values.main, Math.max(600, values.main._clH))) { scrollToBottom(values.main); } return _return; } proto.extendBubble = async function({ content = "", parser = defParser, cb = noOp }) { const values = wk.get(this); if(!values?.[symbols.history]){return} let _return, prev = values.chat[symbols.busy]; let r,j; values.chat[symbols.busy] = new Promise((_r, _j) => {[r, j] = [_r, _j]}) try { prev = await prev; } catch (err) { prev = err } const node = setGetLast(values[symbols.history]), msgText = node?.querySelector(".msg-text"); if(!node){ j(_return = new Error("no bubbles to extend")); return _return; } try { const retVal = await parser({current: node[symbols.raw], incoming: content, parent: msgText, bubble: node, chat: values.chat, prev, component: this}), next = await cb({current: node[symbols.raw], incoming: content, parent: msgText, bubble: node, chat: values.chat, prev, component: this, retVal}); node[symbols.raw] += content; r(_return = next); } catch (err) { j(_return = err) } if (isNearBot(values.main, Math.max(600, values.main._clH))) { scrollToBottom(values.main); } return _return; } proto.clear = function() { if(!this[symbols.input]){return} this[symbols.input].textContent = ""; 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.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"; 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"}`}) }; 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.headerTitle] = el[symbols.headerTitle] || el[symbols.header]?.querySelector(".msger-header-title")?.lastChild; if(v.disabled){ v.input.removeAttribute("contenteditable"); el[symbols.send].innerHTML = `<i class="fa fa-stop"></i>`; el[symbols.send].disabled = true; } else { v.input.setAttribute("contenteditable", "true"); el[symbols.send].innerHTML = `<i class="fa fa-send"></i>` el[symbols.send].disabled = false; } if(v.fileDisabled) { v.attach.disabled = true; } else { v.attach.disabled = false; } if (!v[symbols.typing] && v.typingMs) { el[symbols.typing].firstElementChild.textContent = v.typingName; if (isFinite(v.typingMs)) { v[symbols.typing] = setTimeout(() => { el.removeAttribute("data-typing"); v[symbols.typing] = void(0); }, v.typingMs); } } if (v.headerTitle) { el[symbols.headerTitle].textContent = ` ${v.headerTitle} `; } }; 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 = function(f){ const files = getAttachedFiles(this), textContent = getInputContents(this); f.call(this, {files, textContent, text: textContent}); return this; }; 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" }); 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-chat"> *, *: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))"}; } .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); & 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); font-weight: bold; cursor: pointer; transition: background 0.23s, opacity 0.23s; border:none; border-radius: calc(${v.borderRadius || "var(--border-radius, 4px)"} * 0.75); font-size: 1em; &:disabled { opacity: 0.5; } &:not(:disabled):hover { background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h + 140) calc(s + 30) clamp(25, calc(l - 40), 90) / alpha); } } } .msger-chat { //flex: 1; flex-grow: 1; flex-shrink: 1; overflow-y: auto; padding: ${v.padding || "var(--padding, 10px)"}; position: relative; height: calc(var(--currChatHeight) * 1px); } .msger-chat::-webkit-scrollbar { width: 6px; } .msger-chat::-webkit-scrollbar-track { background: #ddd; } .msger-chat::-webkit-scrollbar-thumb { background: #bdbdbd; } .msg { display: flex; align-items: flex-end; margin-bottom: ${v.margin || "var(--margin, 10px)"}; } .msg:last-of-type { margin: 0; } .msg-img { width: 50px; height: 50px; margin-right: ${v.margin || "var(--margin, 10px)"}; background: #dddddd; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 30) / alpha); background-repeat: no-repeat; background-position: center; background-size: cover; border-radius: 50%; overflow: hidden; display: flex; justify-content: center; align-items: center; text-overflow: ellipsis; word-break: break-word; font-size: 2rem; flex-shrink: 0; } .msg-bubble { flex-shrink: 1; max-width: clamp(0px, calc(100% - 50px), 1024px); padding: calc(${v.padding || "var(--padding, 10px)"} * 1.5); border-radius: calc(${v.borderRadius || "var(--border-radius, 4px)"} * 4); background: #ececec; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 20) / alpha); & img { width: clamp(0px, 100%, 320px); object-fit: cover; aspect-ratio: 1 / 1; display: block; margin: auto; } } .msg-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: ${v.margin || "var(--margin, 10px)"}; } .msg-info-name { margin-right: ${v.margin || "var(--margin, 10px)"}; font-weight: bold; } .msg-info-time { font-size: 0.85em; } .left-msg .msg-bubble { border-bottom-left-radius: 0; color: hsl(from var(--font-color, DarkSlateGray) h s calc(l + 10) / alpha) } .right-msg { flex-direction: row-reverse; } .right-msg .msg-bubble { background: #579ffb; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h - 120) calc(s + 270) calc(l - 10) / alpha); color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 100) / alpha); border-bottom-right-radius: 0; } .right-msg .msg-img { margin: 0 0 0 ${v.margin || "var(--margin, 10px)"}; } .msger-inputarea { //flex-grow: 1; position: relative; flex-wrap: wrap; flex-basis: 2rem; display: flex; padding: ${v.padding || "var(--padding, 10px)"}; border-top: ${v.border || " var(--border, 2px solid #ddd)"} background: #eeeeee; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 5) / alpha); & :is(.msger-upload, .msger-extra) { position: relative; width: 100%; flex-shrink: 1; display: flex; align-items: center; justify-content: start; gap: 4px; padding: 0; &:empty { max-height: 0; padding: 0; } & button { position: relative; border-top-left-radius: 0; border-top-right-radius: 0; padding: 0; height: auto; aspect-ratio: 1 / 1; width: 2rem; display: flex; align-items: center; justify-content: center; background: #dddddd; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s clamp(30, calc(l - 45), 90) / alpha); color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 100) / alpha); font-weight: bold; cursor: pointer; transition: background 0.23s, opacity 0.23s; &:disabled { opacity: 0.5; } &:not(.msger-file):not(:disabled):hover { background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h + 140) calc(s + 30) clamp(25, calc(l - 40), 90) / alpha) } &.msger-file:not(:disabled):hover { opacity: 0.6; } &.msger-file:not(:disabled):hover:after { display: flex; align-items: center; justify-content: center; background: #000000cc; position: absolute; inset: 0; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; content: "\\f1f8"; } } &.msger-upload button { border-bottom-left-radius: 0; border-bottom-right-radius: 0; width: 2.5rem; overflow: hidden; & img { width: 100%; object-fit: cover; aspect-ratio: 1 / 1; display: block; margin: auto; padding: 2px; } } } } .msger-inputarea * { padding: ${v.padding || "var(--padding, 10px)"}; border: none; border-radius: calc(${v.borderRadius || "var(--border-radius, 4px)"} * 0.75); font-size: 1em; } .msger-upload:has(button) + .msger-input { border-top-left-radius: 0; } .msger-input { //width: 100%; width: calc(100% - clamp(calc(2 * var(--padding, 10px)), 8%, 64px) - var(--margin, 10px)); flex-shrink: 1; background: #dddddd; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s clamp(30, calc(l - 45), 90) / alpha); color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 70) / alpha); //max-height: 25dvh; overflow-y: auto; border-bottom-left-radius: 0; &:focus { outline: none; border: 3px solid hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h + 140) calc(s + 30) clamp(25, calc(l - 40), 90) / alpha); border-bottom: none; border-left: none; } } .msger-input[contenteditable=true]:empty:before { content: attr(data-placeholder); pointer-events: none; display: block; } .msger-input p { margin: 0; padding: 0; & * { margin: 0; padding: 0; } } .msger-input::-webkit-scrollbar { width: 6px; } .msger-input::-webkit-scrollbar-track { background: #ddd; } .msger-input::-webkit-scrollbar-thumb { background: #bdbdbd; } .msger-input::placeholder { color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 50) / alpha); } .msger-send-btn { align-self: end; height: auto; aspect-ratio: 1 / 1; width: clamp(calc(2 * var(--padding, 10px)), 8%, 64px); display: flex; align-items: center; justify-content: center; margin-left: ${v.margin || "var(--margin, 10px)"}; background: hsl(160, 100%, 25%); background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h + 140) calc(s + 30) clamp(25, calc(l - 40), 90) / alpha); color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 100) / alpha); font-weight: bold; cursor: pointer; transition: background 0.23s; } .msger-send-btn:hover { background: hsl(160, 100%, 15%); background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} calc(h + 140) calc(s + 30) clamp(10, calc(l - 60), 90) / alpha); } #main-container > svg:first-of-type { width: 100%; height: 100%; pointer-events: none; user-select: none; position: absolute; z-index: -1; } pattern > g { fill: hsl(from ${v.colors.fill || v.colors.background || "var(--bg-color, #f9f9f9)"} h s calc(l - 15) / alpha); stroke: none; opacity: 0.3; } #main-container { position: relative; overflow: hidden; min-height: 100%; } #typing-indicator { position: sticky; bottom: 0; left: 0; display: inline-flex; justify-content: start; align-items: center; overflow: hidden; opacity: 0.5; gap: 20px; background: #dddddd; background: hsl(from ${v.colors.background || "var(--bg-color, #f9f9f9)"} h s clamp(30, calc(l - 45), 90) / alpha); color: #e1e1ff; color: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 70) / alpha); border-radius: calc(${v.borderRadius || "var(--border-radius, 4px)"} * 1.25); border-top-left-radius: 0; padding: calc(${v.padding || "var(--padding, 10px)"} * 0.5); padding-right: calc(${v.padding || "var(--padding, 10px)"} * 2); white-space: nowrap; pointer-events: none; #typing-indicator-name { flex-shrink: 1; overflow: hidden; } .dot-flashing { flex-shrink: 0; } /** * ============================================== * Dot Flashing * https://codepen.io/nzbin/pen/GGrXbp * ============================================== */ .dot-flashing { position: relative; width: 10px; height: 10px; border-radius: 5px; animation: dot-flashing 1s infinite linear alternate; animation-delay: 0.5s; background: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 70) / alpha); } .dot-flashing::before, .dot-flashing::after { content: ""; display: inline-block; position: absolute; width: 10px; height: 10px; border-radius: 5px; top: 0; animation: dot-flashing 1s infinite alternate; background: hsl(from ${v.colors.font || "var(--font-color, DarkSlateGray)"} h s calc(l + 70) / alpha); } .dot-flashing::before { left: -15px; animation-delay: 0s; } .dot-flashing::after { left: 15px; animation-delay: 1s; } /** * ============================================== * Dot Flashing * ============================================== */ } .fa { display: inline-block; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .fa-gear:before, .fa-cog:before { content: "\\f013"; } .fa-comment:before { content: "\\f075"; } .fa-user:before { content: "\\f007"; } .fa-send:before, .fa-paper-plane:before { content: "\\f1d8"; } .fa-stop:before { content: "\\f04d"; } .fa-paperclip:before { content: "\\f0c6"; } .fa-microphone:before { content: "\\f130"; } .fa-file:before { content: "\\f15b"; } .fa-video-camera:before { content: "\\f03d"; } .fa-trash:before { content: "\\f1f8"; } .fa-photo:before, .fa-image:before, .fa-picture-o:before { content: "\\f03e"; } .fa-warning:before, .fa-exclamation-triangle:before { content: "\\f071"; } @keyframes fadein-translate-y { 0% { visibility: visible; transform: translate(0%, 50%); opacity: 0; } 100% { transform: translate(0%,0%); opacity: 1; visibility: visible; } } @keyframes dot-flashing { 0% { opacity: 1; } 50%, 100% { opacity: 0.2; } } @container component-container (min-width: 0px) { .msger-input { max-height: 30cqh; } #typing-indicator { max-width: calc(max(20cqw, 50px) + calc(${v.padding || "var(--padding, 10px)"} * 2.5)); } #typing-indicator-name { max-width: calc(max(20cqw, 50px) + calc(${v.padding || "var(--padding, 10px)"} * 2.5) - 50px); } } </style> `}` }} |> await ${({values:v}) => async () => ch(shadow).append(v.section = ch.dom` <section class="msger"> <header class="msger-header"> <div class="msger-header-title"> <i class="fa fa-comment"></i> ${v.headerTitle || "SimpleChat"} </div> <button class="msger-header-options" disabled> <span><i class="fa fa-cog"></i></span> </button> </header> <main class="msger-chat"> <div id="main-container"> </div> <div id="typing-indicator"> <div id="typing-indicator-name">test</div> <div class="dot-flashing"></div> </div> </main> <form class="msger-inputarea"> <!--<input type="text" class="msger-input" placeholder="Enter your message...">--> <div class="msger-upload"></div> <div class="msger-input" data-placeholder="Message..." contenteditable=true spellcheck="false" translate="no"></div> <button type="button" class="msger-send-btn"><i class="fa fa-send"></i></button> <div class="msger-extra"> <button type="button"><i class="fa fa-paperclip"></i></button> <button type="button" disabled><i class="fa fa-microphone"></i></button> </div> </form> </section> `)} |> await ${({values:v}) => async() => { const editable = v.input = v.section.querySelector(".msger-input[contenteditable]"); ch(editable).on("input", function(e){ if(!(this.textContent ?? "").length){ if(this.hasChildNodes()){ for (let node of this.childNodes){ if(node.tagName.toLowerCase() !== "p"){ this.replaceChild(ch[`p{ "prop": [["innerHTML", "<br>"]] }`], node); } } } return } if(this.firstElementChild?.tagName.toLowerCase() !== "p"){ const content = this.textContent, p = ch.p; p.textContent = content; this.replaceChildren(p); const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(p); range.collapse(false); // Collapse the range to the end selection?.removeAllRanges(); selection?.addRange(range); //selection?.collapseToEnd?.(); this.focus(); } }); }} |> await ${({values:v}) => async() => { v.upload = ch.select(".msger-upload", v.section).selected; v.extra = ch.select(".msger-extra", v.section).selected; v.attach = ch.select("button:has(i[class~=fa-paperclip])").selected; ch.on("click", function(e){return attachFile.call(this, {event:e, values:v})}); }} |> await ${({values:v}) => async() => v.main = v.section.querySelector("main")} |> await ${({values:v}) => async () => ch(v.chat = v.main.firstElementChild).prepend(v.svg = ch.dom` <svg id="main-svg" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice"> <defs> <foreignObject> <slot></slot> </foreignObject> <pattern id="bgPattern" patternTransform="scale(3)" patternContentUnits="userSpaceOnUse" patternUnits="userSpaceOnUse" width="95" height="74"> <g> <path d="m16 46c0.15-0.24 0.54-0.35 0.77-0.22 0.41 0.23 0.32 0.98-0.12 1.1-0.49 0.13-0.92-0.44-0.65-0.88zm0.25 0.61c0.2 0.17 0.51 0.075 0.6-0.18 0.15-0.46-0.42-0.75-0.67-0.35-0.14 0.23-0.13 0.37 0.075 0.53zm1.5 2.7c0.16-0.71 0.98-0.73 1.2-0.028 0.14 0.53 0.017 0.81-0.42 0.93-0.43 0.12-0.86-0.4-0.75-0.9zm0.46 0.69c0.26 0.14 0.5 0.019 0.56-0.29 0.1-0.47-0.16-0.81-0.54-0.71-0.42 0.11-0.44 0.78-0.025 1zm0.83-3.7c-0.099-1.8 1.1-3 3-3.1 0.63-0.0048 0.63-0.0048 0.29-0.51-2.4-3.6 1.2-8.1 4.8-6.1 3.6 2 1.1 8.5-2.8 7.1-0.34-0.12-0.34-0.12-0.092 0.32 1 1.8 0.94 3.2-0.22 5.3-0.45 0.79-1.1 0.59-1.1-0.32 0.0073-0.14-0.015-0.16-0.17-0.11-0.27 0.072-0.62 0.0025-1.4-0.27-0.68-0.25-0.68-0.25-0.73-0.065-0.25 1.1-1.6-0.72-1.7-2.3zm0.67 1.6c0.47 0.83 0.77 1 0.82 0.51 0.025-0.24 0.19-0.25 0.73-0.039 1.3 0.49 1.9 0.53 2 0.12 0.025-0.14 0.089-0.32 0.14-0.4 0.053-0.082 0.09-0.18 0.081-0.21-0.019-0.069-1.2-0.9-1.5-1.1-0.25-0.13-1.2-0.81-1.5-1.1-0.28-0.27-0.36 0.11-0.18 0.82 0.16 0.61 0.44 0.89 1.1 1.1 0.25 0.09 0.68 0.25 0.96 0.35 0.5 0.18 0.78 0.38 0.72 0.5-0.046 0.08-0.3 8.1e-4 -1-0.33-0.34-0.15-0.78-0.33-0.98-0.41-0.42-0.16-0.88-0.63-0.98-1-0.16-0.6-0.086-1.5 0.13-1.5 0.051-0.014 0.17 0.059 0.26 0.16 0.3 0.34 0.99 0.88 1.6 1.3 0.33 0.2 0.76 0.48 0.95 0.62 0.35 0.25 0.35 0.25 0.43 0.075 0.13-0.29-0.53-0.96-1.5-1.5-0.23-0.13-0.6-0.38-0.84-0.55-0.23-0.17-0.45-0.32-0.48-0.32-0.4-0.051 0.015-0.62 0.56-0.76 1.7-0.44 3.3 1.4 2.7 2.9-0.065 0.15-0.16 0.42-0.2 0.59-0.047 0.17-0.17 0.46-0.27 0.64-0.3 0.56-0.32 1.1-0.027 1.2 0.28 0.066 0.76-0.71 1.2-2 0.44-1.2 0.18-2.4-0.89-4-0.14-0.21 0.1-0.23 0.54-0.05 3.7 1.6 6.3-4.7 2.8-6.7-3.5-2-7 2.6-4.4 6 0.48 0.64 0.48 0.76 0.033 0.69-2.7-0.41-4.3 2-2.9 4.4zm2.8-7.4c-0.04-0.54 0.079-0.61 0.43-0.26 0.41 0.41 1.6 1.3 2.5 1.8 1 0.61 1 0.68 0.14 0.92-1.4 0.38-3-0.84-3.1-2.5zm0.61 1.3c0.55 0.7 2 1.2 2.7 0.92 0.25-0.11 0.25-0.11-0.48-0.56-0.76-0.46-2.1-1.4-2.4-1.7-0.23-0.22-0.27-0.19-0.22 0.19 0.024 0.18 0.047 0.37 0.051 0.43 0.013 0.17 0.16 0.47 0.36 0.72zm-0.38-2.9c0.21-1 0.34-1 1.6-0.031 0.56 0.43 1.5 1 2.2 1.5 1.4 0.8 1.3 0.69 1.1 1.1-0.36 0.75-0.56 0.78-1.5 0.23-0.78-0.44-3.4-2.4-3.4-2.5-0.0017-0.0095 0.018-0.12 0.043-0.24zm2 1.6c1.7 1.3 2.3 1.5 2.6 0.99 0.24-0.39 0.24-0.39-0.87-1-0.61-0.36-1.6-1-2.2-1.4-0.57-0.43-1.1-0.76-1.1-0.73-0.5 0.53-0.29 0.84 1.5 2.2zm-0.72-3.1c1.4-1.1 3.7 0.25 3.9 2.2 0.078 0.82-0.53 0.61-2.7-0.97-1.5-1.1-1.5-1.1-1.2-1.3zm1.1 0.99c0.93 0.69 2.5 1.6 2.6 1.6 0.2-0.053-0.15-1.2-0.51-1.6-0.9-1.1-2-1.5-2.9-0.93-0.29 0.17-0.34 0.11 0.81 0.96zm-3.1 7.2c0.48 0.28 1.4 1 1.5 1.3 0.3 0.45 0.38-0.86 0.082-1.4-0.47-0.9-2.3-1.5-2.8-0.86-0.13 0.15 0.31 0.55 1.2 1zm1.6 1.8c0.058-0.15 0.1-0.29 0.093-0.3-0.023-0.069-0.24 0.42-0.22 0.5 0.011 0.041 0.068-0.046 0.13-0.19zm7.1-9.1c0.23-2 3.2 0.43 1.1 0.99-0.6 0.16-1.1-0.35-1.1-0.99zm0.52 0.73c2 1.1 0.4-2.8-0.31-0.83-0.1 0.28 0.051 0.69 0.31 0.83z" stroke-width=".4"/> <path d="m47 24c0.15-0.24 0.54-0.35 0.77-0.22 0.41 0.23 0.32 0.98-0.12 1.1-0.49 0.13-0.92-0.44-0.65-0.88zm0.25 0.61c0.2 0.17 0.51 0.075 0.6-0.18 0.15-0.46-0.42-0.75-0.67-0.35-0.14 0.23-0.13 0.37 0.075 0.53zm1.5 2.7c0.16-0.71 0.98-0.73 1.2-0.028 0.14 0.53 0.017 0.81-0.42 0.93-0.43 0.12-0.86-0.4-0.75-0.9zm0.46 0.69c0.26 0.14 0.5 0.019 0.56-0.29 0.1-0.47-0.16-0.81-0.54-0.71-0.42 0.11-0.44 0.78-0.025 1zm0.83-3.7c-0.099-1.8 1.1-3 3-3.1 0.63-0.0048 0.63-0.0048 0.29-0.51-2.4-3.6 1.2-8.1 4.8-6.1 3.6 2 1.1 8.5-2.8 7.1-0.34-0.12-0.34-0.12-0.092 0.32 1 1.8 0.94 3.2-0.22 5.3-0.45 0.79-1.1 0.59-1.1-0.32 0.0073-0.14-0.015-0.16-0.17-0.11-0.27 0.072-0.62 0.0025-1.4-0.27-0.68-0.25-0.68-0.25-0.73-0.065-0.25 1.1-1.6-0.72-1.7-2.3zm0.67 1.6c0.47 0.83 0.77 1 0.82 0.51 0.025-0.24 0.19-0.25 0.73-0.039 1.3 0.49 1.9 0.53 2 0.12 0.025-0.14 0.089-0.32 0.14-0.4 0.053-0.082 0.09-0.18 0.081-0.21-0.019-0.069-1.2-0.9-1.5-1.1-0.25-0.13-1.2-0.81-1.5-1.1-0.28-0.27-0.36 0.11-0.18 0.82 0.16 0.61 0.44 0.89 1.1 1.1 0.25 0.09 0.68 0.25 0.96 0.35 0.5 0.18 0.78 0.38 0.72 0.5-0.046 0.08-0.3 8.1e-4 -1-0.33-0.34-0.15-0.78-0.33-0.98-0.41-0.42-0.16-0.88-0.63-0.98-1-0.16-0.6-0.086-1.5 0.13-1.5 0.051-0.014 0.17 0.059 0.26 0.16 0.3 0.34 0.99 0.88 1.6 1.3 0.33 0.2 0.76 0.48 0.95 0.62 0.35 0.25 0.35 0.25 0.43 0.075 0.13-0.29-0.53-0.96-1.5-1.5-0.23-0.13-0.6-0.38-0.84-0.55-0.23-0.17-0.45-0.32-0.48-0.32-0.4-0.051 0.015-0.62 0.56-0.76 1.7-0.44 3.3 1.4 2.7 2.9-0.065 0.15-0.16 0.42-0.2 0.59-0.047 0.17-0.17 0.46-0.27 0.64-0.3 0.56-0.32 1.1-0.027 1.2 0.28 0.066 0.76-0.71 1.2-2 0.44-1.2 0.18-2.4-0.89-4-0.14-0.21 0.1-0.23 0.54-0.05 3.7 1.6 6.3-4.7 2.8-6.7-3.5-2-7 2.6-4.4 6 0.48 0.64 0