@cimo/jsmvcfw
Version:
Javascript mvc framework. Light, fast and secure.
211 lines • 8.32 kB
JavaScript
const safeHtml = (html) => {
const template = document.createElement("template");
template.innerHTML = html;
const blockElementList = ["script", "iframe", "object", "embed", "applet", "meta", "link", "style"].join(",");
template.content.querySelectorAll(blockElementList).forEach((element) => element.remove());
const elementList = template.content.querySelectorAll("*");
for (const element of elementList) {
const attributeList = [...element.attributes];
for (const attribute of attributeList) {
const name = attribute.name.toLowerCase();
const value = attribute.value.trim();
if (name.startsWith("on")) {
element.removeAttribute(attribute.name);
continue;
}
if (name === "href" || name === "src" || name === "xlink:href" || name === "action" || name === "formaction") {
const normalized = value.replace(/\s+/g, "").toLowerCase();
const isJs = normalized.startsWith("javascript:");
const isHtmlData = normalized.startsWith("data:text/html");
const isUnsafeData = normalized.startsWith("data:") && !normalized.startsWith("data:image/");
if (isJs || isHtmlData || isUnsafeData) {
element.removeAttribute(attribute.name);
}
}
if (name === "srcdoc") {
element.removeAttribute(attribute.name);
}
}
}
return template.innerHTML;
};
const applyProperty = (element, key, valueNew, valueOld) => {
if (key === "jsmvcfw-html") {
if (typeof valueNew === "string") {
element.innerHTML = safeHtml(valueNew);
}
else {
element.innerHTML = "";
}
return;
}
if (key.startsWith("on") && typeof valueNew === "function") {
const eventName = key.slice(2).toLowerCase();
if (typeof valueOld === "function") {
element.removeEventListener(eventName, valueOld);
}
element.addEventListener(eventName, valueNew);
}
else if (typeof valueNew === "boolean") {
if (key === "selected" && "selected" in element) {
element.selected = valueNew;
}
else if (key === "checked" && "checked" in element) {
element.checked = valueNew;
}
valueNew ? element.setAttribute(key, "") : element.removeAttribute(key);
}
else if (typeof valueNew === "string" || typeof valueNew === "number") {
if (key === "value" && "value" in element) {
element.value = valueNew.toString();
}
element.setAttribute(key, valueNew.toString());
}
else if (Array.isArray(valueNew)) {
let stringValue = "";
for (const value of valueNew) {
if (typeof value === "string") {
stringValue += value + " ";
}
}
element.setAttribute(key, stringValue.trim());
}
else if (valueNew == null) {
if (key === "selected" && "selected" in element) {
element.selected = false;
}
else if (key === "checked" && "checked" in element) {
element.checked = false;
}
else if (key === "value" && "value" in element) {
element.value = "";
}
element.removeAttribute(key);
}
};
const updateProperty = (element, oldList, newList) => {
for (const key in oldList) {
if (!(key in newList)) {
if (key === "jsmvcfw-html") {
element.innerHTML = "";
}
else if (key.startsWith("on") && typeof oldList[key] === "function") {
element.removeEventListener(key.slice(2).toLowerCase(), oldList[key]);
}
else {
element.removeAttribute(key);
}
}
}
for (const [key, value] of Object.entries(newList)) {
const valueOld = oldList[key];
if (value !== valueOld) {
applyProperty(element, key, value, valueOld);
}
}
};
const updateChildren = (element, nodeOldListValue, nodeNewListValue) => {
const nodeOldList = Array.isArray(nodeOldListValue) ? nodeOldListValue : [];
const nodeNewList = Array.isArray(nodeNewListValue) ? nodeNewListValue : [];
const keyOldObject = {};
for (let a = 0; a < nodeOldList.length; a++) {
const node = nodeOldList[a];
if (typeof node === "object" && node.key) {
keyOldObject[node.key] = { node, dom: element.childNodes[a] };
}
}
const nodeMaxLength = Math.max(nodeOldList.length, nodeNewList.length);
for (let a = 0; a < nodeMaxLength; a++) {
const nodeOld = nodeOldList[a];
const nodeNew = nodeNewList[a];
const nodeDom = element.childNodes[a];
if (!nodeNew && nodeDom) {
const isControllerName = nodeDom.nodeType === Node.ELEMENT_NODE && nodeDom.hasAttribute("jsmvcfw-controllername");
if (!isControllerName) {
element.removeChild(nodeDom);
}
continue;
}
if (typeof nodeNew === "string") {
if (!nodeDom) {
element.appendChild(document.createTextNode(nodeNew));
}
else if (nodeDom.nodeType === Node.TEXT_NODE) {
if (nodeDom.textContent !== nodeNew) {
nodeDom.textContent = nodeNew;
}
}
else {
element.replaceChild(document.createTextNode(nodeNew), nodeDom);
}
}
else if (typeof nodeNew === "object") {
const isElementNode = nodeDom !== null && nodeDom !== undefined && nodeDom.nodeType === Node.ELEMENT_NODE;
const isControllerName = isElementNode && nodeDom.hasAttribute("jsmvcfw-controllername");
if (isControllerName && !nodeNew.key) {
continue;
}
if (nodeNew.key && keyOldObject[nodeNew.key]) {
const { node, dom } = keyOldObject[nodeNew.key];
updateVirtualNode(dom, node, nodeNew);
if (nodeDom !== dom) {
element.insertBefore(dom, nodeDom);
}
}
else if (typeof nodeOld === "object" && nodeOld.tag === nodeNew.tag && nodeDom) {
updateVirtualNode(nodeDom, nodeOld, nodeNew);
}
else {
const domNew = createVirtualNode(nodeNew);
if (nodeDom) {
element.replaceChild(domNew, nodeDom);
}
else {
element.appendChild(domNew);
}
}
}
}
while (element.childNodes.length > nodeNewList.length) {
const nodeExtra = element.childNodes[nodeNewList.length];
const isControllerName = nodeExtra.nodeType === Node.ELEMENT_NODE && nodeExtra.hasAttribute("jsmvcfw-controllername");
if (!isControllerName) {
element.removeChild(nodeExtra);
}
else {
break;
}
}
};
export const createVirtualNode = (node) => {
const element = document.createElement(node.tag);
for (const [key, value] of Object.entries(node.propertyObject || {})) {
applyProperty(element, key, value);
}
if (Array.isArray(node.childrenList)) {
for (const child of node.childrenList) {
if (typeof child === "string") {
element.appendChild(document.createTextNode(child));
}
else {
element.appendChild(createVirtualNode(child));
}
}
}
return element;
};
export const updateVirtualNode = (element, nodeOld, nodeNew) => {
if (nodeOld.tag !== nodeNew.tag) {
const elementNew = createVirtualNode(nodeNew);
element.replaceWith(elementNew);
return;
}
const propertyOld = nodeOld.propertyObject || {};
const propertyNew = nodeNew.propertyObject || {};
updateProperty(element, propertyOld || {}, propertyNew || {});
if ("jsmvcfw-html" in propertyNew) {
return;
}
updateChildren(element, nodeOld.childrenList, nodeNew.childrenList);
};
//# sourceMappingURL=JsMvcFwDom.js.map