import * as dom from "./utils_dom.js"; import { getObjectValue } from "./shared_utils.js"; const CONFIG_DEFAULT = { attrBind: "data-bind", attrIf: "data-if", attrIfIs: "data-if-is", }; const CONFIG = Object.assign({}, CONFIG_DEFAULT, { attrBind: "bind", attrIf: "if", attrIfIs: "if-is", }); const RGX_COMPARISON = (() => { let value = "((?:\\!*)[_a-z0-9\\.\\-\\[\\]'\"]+)"; let comparison = "((?:<|>|==|\\!=)=?)"; return new RegExp(`^(?:\\!*)\\(?${value}\\s*${comparison}\\s*${value}\\)?$`, "i"); })(); const RGXPART_BIND_FN_TEMPLATE_STRING = "template|tpl"; const RGXPART_BIND_FN_ELEMENT_STRING = "element|el"; const RGX_BIND_FN_TEMPLATE = new RegExp(`^(?:${RGXPART_BIND_FN_TEMPLATE_STRING})\\(([^\\)]+)\\)`, "i"); const RGX_BIND_FN_ELEMENT = new RegExp(`^(?:${RGXPART_BIND_FN_ELEMENT_STRING})\\(([^\\)]+)\\)`, "i"); const RGX_BIND_FN_TEMPLATE_OR_ELEMENT = new RegExp(`^(?:${RGXPART_BIND_FN_TEMPLATE_STRING}|${RGXPART_BIND_FN_ELEMENT_STRING})\\(([^\\)]+)\\)`, "i"); const RGX_BIND_FN_LENGTH = /^(?:length|len|size)\(([^\)]+)\)/i; const RGX_BIND_FN_FORMAT = /^(?:format|fmt)\(([^\,]+),([^\)]+)\)/i; const RGX_BIND_FN_CALL = /^([^\(]+)\(([^\)]*)\)/i; const EMPTY_PREPROCESS_FN = (data) => data; const RGX_BIND_DECLARATIONS = /\s*(\!*(?:[\$_a-z0-9-\.\'\"]|\?\?|\|\||\&\&|(?:(?:<|>|==|\!=)=?))+(?:\`[^\`]+\`)?(?:\([^\)]*\))?)(?::(.*?))?(\s|$)/gi; function localAssertNotFalsy(input, errorMsg = `Input is not of type.`) { if (input == null) { throw new Error(errorMsg); } return input; } function cleanKey(key) { return key.toLowerCase().trim().replace(/\s/g, ""); } function toArray(value) { if (Array.isArray(value)) { return value; } if (value instanceof Set) { return Array.from(value); } if (typeof value === "object" && typeof value.length === "number") { return [].slice.call(value); } return [value]; } function flattenArray(arr) { return toArray(arr).reduce((acc, val) => { return acc.concat(Array.isArray(val) ? flattenArray(val) : val); }, []); } function getObjValue(lookup, obj) { let booleanMatch = lookup.match(/^(\!+)(.+?)$/i) || []; let booleanNots = []; if (booleanMatch[1] && booleanMatch[2]) { booleanNots = booleanMatch[1].split(""); lookup = booleanMatch[2]; } let value = getObjectValue(obj, lookup); while (booleanNots.length) { value = !value; booleanNots.shift(); } return value; } function getPrimitiveOrObjValue(stringValue, data) { let value; if (stringValue == null) { return stringValue; } let negate = getNegates(stringValue); if (negate != null) { stringValue = stringValue.replace(/^\!+/, ""); } try { const cleanedStringValue = stringValue.replace(/^'(.*)'$/, '"$1"'); value = JSON.parse(cleanedStringValue); } catch (e) { value = getObjValue(stringValue, data); } value = negate !== null ? (negate === 1 ? !value : !!value) : value; return value; } function getNegates(stringValue) { let negate = null; let negateMatches = stringValue.match(/^(\!+)(.*)/); if (negateMatches && negateMatches.length >= 3) { negate = negateMatches[1].length % 2; } return negate; } function getStringComparisonExpression(bindingPropName, data) { let comparisonMatches = bindingPropName.match(RGX_COMPARISON); if (!(comparisonMatches === null || comparisonMatches === void 0 ? void 0 : comparisonMatches.length)) { return null; } let a = getPrimitiveOrObjValue(comparisonMatches[1], data); let b = getPrimitiveOrObjValue(comparisonMatches[3], data); let c = comparisonMatches[2]; let value = (() => { switch (c) { case "===": return a === b; case "==": return a == b; case "<=": return a <= b; case ">=": return a >= b; case "!==": return a !== b; case "!=": return a != b; case "<": return a < b; case ">": return a > b; default: return undefined; } })(); return value; } function replaceTplElementWithChildren(tplEl, fragOrElOrEls) { const els = Array.isArray(fragOrElOrEls) ? fragOrElOrEls : [fragOrElOrEls]; tplEl.replaceWith(...els); const numOfChildren = Array.isArray(fragOrElOrEls) ? fragOrElOrEls.length : fragOrElOrEls.childElementCount; if (numOfChildren === 1) { const firstChild = Array.isArray(fragOrElOrEls) ? fragOrElOrEls[0] : fragOrElOrEls.firstElementChild; if (firstChild instanceof Element) { if (tplEl.className.length) { firstChild.className += ` ${tplEl.className}`; } let attr = tplEl.getAttribute("data"); if (attr) { firstChild.setAttribute("data", attr); } attr = tplEl.getAttribute(CONFIG.attrBind); if (attr) { firstChild.setAttribute(CONFIG.attrBind, attr); } } } } function getValueForBinding(bindingPropName, context) { console.log("getValueForBinding", bindingPropName, context); const data = context.data; let stringTemplate = null; let stringTemplates = /^(.*?)\`([^\`]*)\`$/.exec(bindingPropName.trim()); if ((stringTemplates === null || stringTemplates === void 0 ? void 0 : stringTemplates.length) === 3) { bindingPropName = stringTemplates[1]; stringTemplate = stringTemplates[2]; } let value = null; let hadALogicalOp = false; const opsToValidation = new Map([ [/\s*\?\?\s*/, (v) => v != null], [/\s*\|\|\s*/, (v) => !!v], [/\s*\&\&\s*/, (v) => !v], ]); for (const [op, fn] of opsToValidation.entries()) { if (bindingPropName.match(op)) { hadALogicalOp = true; const bindingPropNames = bindingPropName.split(op); for (const propName of bindingPropNames) { value = getValueForBindingPropName(propName, context); if (fn(value)) { break; } } break; } } if (!hadALogicalOp) { value = getValueForBindingPropName(bindingPropName, context); } return stringTemplate && value != null ? stringTemplate.replace(/\$\{value\}/g, String(value)) : value; } function getValueForBindingPropName(bindingPropName, context) { var _a, _b, _c; const data = context.data; let negate = getNegates(bindingPropName); if (negate != null) { bindingPropName = bindingPropName.replace(/^\!+/, ""); } let value; RGX_COMPARISON.lastIndex = 0; if (RGX_COMPARISON.test(bindingPropName)) { value = getStringComparisonExpression(bindingPropName, data); } else if (RGX_BIND_FN_LENGTH.test(bindingPropName)) { bindingPropName = RGX_BIND_FN_LENGTH.exec(bindingPropName)[1]; value = getPrimitiveOrObjValue(bindingPropName, data); value = (value && value.length) || 0; } else if (RGX_BIND_FN_FORMAT.test(bindingPropName)) { let matches = RGX_BIND_FN_FORMAT.exec(bindingPropName); bindingPropName = matches[1]; value = getPrimitiveOrObjValue(bindingPropName, data); value = matches[2].replace(/^['"]/, "").replace(/['"]$/, "").replace(/\$1/g, value); } else if (RGX_BIND_FN_CALL.test(bindingPropName)) { console.log("-----"); console.log(bindingPropName); let matches = RGX_BIND_FN_CALL.exec(bindingPropName); const functionName = matches[1]; const maybeDataName = (_a = matches[2]) !== null && _a !== void 0 ? _a : null; value = getPrimitiveOrObjValue(maybeDataName, data); console.log(functionName, maybeDataName, value); if (typeof (value === null || value === void 0 ? void 0 : value[functionName]) === "function") { value = value[functionName](value, data, context.currentElement, context.contextElement); } else if (typeof (data === null || data === void 0 ? void 0 : data[functionName]) === "function") { value = data[functionName](value, data, context.currentElement, context.contextElement); } else if (typeof ((_b = context.currentElement) === null || _b === void 0 ? void 0 : _b[functionName]) === "function") { value = context.currentElement[functionName](value, data, context.currentElement, context.contextElement); } else if (typeof ((_c = context.contextElement) === null || _c === void 0 ? void 0 : _c[functionName]) === "function") { value = context.contextElement[functionName](value, data, context.currentElement, context.contextElement); } else { console.error(`No method named ${functionName} on data or element instance. Just calling regular value.`); value = getPrimitiveOrObjValue(bindingPropName, data); } } else { value = getPrimitiveOrObjValue(bindingPropName, data); } if (value !== undefined) { value = negate !== null ? (negate === 1 ? !value : !!value) : value; } return value; } function removeBindingAttributes(elOrEls, deep = false) { flattenArray(elOrEls || []).forEach((el) => { el.removeAttribute(CONFIG.attrBind); const innerBinds = dom.queryAll(`:scope [${CONFIG.attrBind}]`, el); const innerTplBinds = deep ? [] : dom.queryAll(`:scope [data-tpl] [${CONFIG.attrBind}]`); innerBinds.forEach((el) => { if (deep || !innerTplBinds.includes(el)) { el.removeAttribute(CONFIG.attrBind); } }); }); } const templateCache = {}; export function checkKey(key) { return !!templateCache[cleanKey(key)]; } export function register(key, htmlOrElement = null, preProcessScript) { key = cleanKey(key); if (templateCache[key]) { return templateCache[key]; } let fragment = null; if (typeof htmlOrElement === "string") { const frag = document.createDocumentFragment(); if (htmlOrElement.includes("<")) { const html = htmlOrElement.trim(); const htmlParentTag = (html.startsWith(" { const dataTplAttr = el.getAttribute("data-tpl"); if (dataTplAttr) { const tpls = dataTplAttr.split(" "); tpls.forEach((tpl) => { const dataAttribute = el.getAttribute("data") || ""; const childData = (dataAttribute && getObjValue(dataAttribute, data)) || data; bind(el, childData, bindOptions); }); } else { bind(el, data, bindOptions); } }); } export function bind(elOrEls, data = {}, bindOptions = {}) { var _a; if (elOrEls instanceof HTMLElement) { data = getPreProcessScript(elOrEls)({ ...data }); } if (typeof data !== "object") { data = { value: data }; if (elOrEls instanceof HTMLElement && elOrEls.children.length === 0 && !elOrEls.getAttribute(CONFIG.attrBind)) { dom.setAttributes(elOrEls, { [CONFIG.attrBind]: "value" }); } } let passedEls = !Array.isArray(elOrEls) ? [elOrEls] : elOrEls; for (const el of passedEls) { const conditionEls = toArray(dom.queryAll(`[${CONFIG.attrIf}]`, el)); const contextElement = (_a = bindOptions.contextElement) !== null && _a !== void 0 ? _a : (el instanceof ShadowRoot ? el.host : el); for (const conditionEl of conditionEls) { getValueForBindingPropName; let isTrue = getValueForBinding(conditionEl.getAttribute(CONFIG.attrIf), { data, contextElement: contextElement, currentElement: conditionEl, }); conditionEl.setAttribute(CONFIG.attrIfIs, String(!!isTrue)); } let toBindEls = toArray(dom.queryAll(`:not([${CONFIG.attrIfIs}="false"]) [${CONFIG.attrBind}]:not([data-tpl]):not([${CONFIG.attrIfIs}="false"])`, el)); if (el instanceof HTMLElement && el.getAttribute(CONFIG.attrBind)) { toBindEls.unshift(el); } if (toBindEls.length) { let innerBindsElements = dom.queryAll(`:scope [data-tpl] [${CONFIG.attrBind}], :scope [data-autobind="false"] [${CONFIG.attrBind}]`, el); toBindEls = toBindEls.filter((maybeBind) => !innerBindsElements.includes(maybeBind)); toBindEls.forEach((child) => { RGX_BIND_DECLARATIONS.lastIndex = 0; let bindings = []; let bindingMatch; while ((bindingMatch = RGX_BIND_DECLARATIONS.exec(child.getAttribute(CONFIG.attrBind).replace(/\s+/, " ").trim())) !== null) { bindings.push([bindingMatch[1], bindingMatch[2]]); } bindings.forEach((bindings) => { let bindingDataProperty = localAssertNotFalsy(bindings.shift()); let bindingFields = ((bindings.length && bindings[0]) || "default") .trim() .replace(/^\[(.*?)\]$/i, "$1") .split(","); let value = getValueForBinding(bindingDataProperty, { data, contextElement: contextElement, currentElement: child, }); if (value === undefined) { if (bindOptions.onlyDefined === true) { return; } else { value = null; } } bindingFields.forEach((field) => { if (field.startsWith("style.")) { let stringVal = String(value); if (value && !stringVal.includes("url(") && stringVal !== "none" && (field.includes("background-image") || stringVal.startsWith("http"))) { value = `url(${value})`; } dom.setStyle(child, field.replace("style.", ""), value); } else if (field.startsWith("el.")) { if (field === "el.remove") { if (value === true) { child.remove(); } } else if (field === "el.toggle") { dom.setStyle(child, "display", value === true ? "" : "none"); } else if (field.startsWith("el.classList.toggle")) { const cssClass = field.replace(/el.classList.toggle\(['"]?(.*?)['"]?\)/, "$1"); child.classList.toggle(cssClass, !!value); } } else if (RGX_BIND_FN_TEMPLATE_OR_ELEMENT.test(field)) { dom.empty(child); let elementOrTemplateName = RGX_BIND_FN_TEMPLATE_OR_ELEMENT.exec(field)[1]; if (Array.isArray(value) || value instanceof Set) { const arrayVals = toArray(value); let isElement = RGX_BIND_FN_ELEMENT.test(field); let frag = document.createDocumentFragment(); arrayVals.forEach((item, index) => { let itemData; if (typeof item === "object") { itemData = Object.assign({ $index: index }, item); } else { itemData = { $index: index, value: item }; } const els = bindToElOrTemplate(elementOrTemplateName, itemData); frag.append(...els); }); if (child.nodeName.toUpperCase() === "TPL") { replaceTplElementWithChildren(child, frag); } else { dom.empty(child).appendChild(frag); } } else if (value) { const els = bindToElOrTemplate(elementOrTemplateName, value); if (child.nodeName.toUpperCase() === "TPL") { replaceTplElementWithChildren(child, els); } else { child.append(...els); } } } else { dom.setAttributes(child, { [field]: value }); } }); }); }); } if (bindOptions.singleScoped !== true) { let toInitEls = toArray(el.querySelectorAll(":scope *[data-tpl]")); if (toInitEls.length) { let innerInits = dom.queryAll(':scope *[data-tpl] *[data-tpl], :scope [data-autobind="false"] [data-tpl]', el); toInitEls = toInitEls.filter((maybeInitEl) => { if (innerInits.includes(maybeInitEl)) { return false; } let tplKey = maybeInitEl.getAttribute("data-tpl"); if (data && (data[tplKey] || data[tplKey.replace("tpl:", "")])) { return true; } return maybeInitEl.getAttribute("data-autobind") !== "false"; }); toInitEls.forEach((toInitEl) => { var tplKey = toInitEl.getAttribute("data-tpl"); init(toInitEl, (data && (data[tplKey] || data[tplKey.replace("tpl:", "")])) || data); }); } } } } function bindToElOrTemplate(elementOrTemplateName, data) { let el = getTemplateFragment(elementOrTemplateName); if (!el) { el = dom.createElement(elementOrTemplateName, data); } else { el = inflateOnce(el, data, { skipInit: false }); } const els = (Array.isArray(el) ? el : [el]).filter((el) => !!el); els.forEach((el) => { el.removeAttribute("data-tpl"); let toBindEls = dom.queryAll("[data-tpl]", el); let innerBindsElements = dom.queryAll(`:scope [data-tpl] [${CONFIG.attrBind}], :scope [data-autobind="false"] [${CONFIG.attrBind}]`, el); toBindEls = toBindEls.filter((maybeBind) => !innerBindsElements.includes(maybeBind)); toBindEls.forEach((c) => c.removeAttribute("data-tpl")); }); return els; }