"use strict"; require("../util/polyfill.js"); const api = require("../api.js"); const templates = require("../templates.js"); const domParser = new DOMParser(); const misc = require("./misc.js"); const uri = require("./uri.js"); function _imbueId(options) { if (!options.id) { options.id = "gen-" + Math.random().toString(36).substring(7); } } function _makeLabel(options, attrs) { if (!options.text) { return ""; } if (!attrs) { attrs = {}; } attrs.for = options.id; return makeElement("label", attrs, options.text); } function makeFileSize(fileSize) { return misc.formatFileSize(fileSize); } function makeMarkdown(text) { return misc.formatMarkdown(text); } function makeRelativeTime(time) { return makeElement( "time", { datetime: time, title: time }, misc.formatRelativeTime(time) ); } function makeThumbnail(url) { return makeElement( "span", url ? { class: "thumbnail", style: `background-image: url(\'${url}\')`, } : { class: "thumbnail empty" }, makeElement("img", { alt: "thumbnail", src: url }) ); } function makeRadio(options) { _imbueId(options); return makeElement( "label", { for: options.id }, makeElement("input", { id: options.id, name: options.name, value: options.value, type: "radio", checked: options.selectedValue === options.value, disabled: options.readonly, required: options.required, }), makeElement("span", { class: "radio" }, options.text) ); } function makeCheckbox(options) { _imbueId(options); return makeElement( "label", { for: options.id }, makeElement("input", { id: options.id, name: options.name, value: options.value, type: "checkbox", checked: options.checked !== undefined ? options.checked : false, disabled: options.readonly, required: options.required, }), makeElement("span", { class: "checkbox" }, options.text) ); } function makeSelect(options) { return ( _makeLabel(options) + makeElement( "select", { id: options.id, name: options.name, disabled: options.readonly, }, ...Object.keys(options.keyValues).map((key) => makeElement( "option", { value: key, selected: key === options.selectedKey }, options.keyValues[key] ) ) ) ); } function makeInput(options) { options.value = options.value || ""; return _makeLabel(options) + makeElement("input", options); } function makeButton(options) { options.type = "button"; return makeInput(options); } function makeTextInput(options) { options.type = "text"; return makeInput(options); } function makeTextarea(options) { const value = options.value || ""; delete options.value; return _makeLabel(options) + makeElement("textarea", options, value); } function makePasswordInput(options) { options.type = "password"; return makeInput(options); } function makeEmailInput(options) { options.type = "email"; return makeInput(options); } function makeColorInput(options) { const textInput = makeElement("input", { type: "text", value: options.value || "", required: options.required, class: "color", }); const backgroundPreviewNode = makeElement("div", { class: "preview background-preview", style: `border-color: ${options.value}; background-color: ${options.value}`, }); const textPreviewNode = makeElement("div", { class: "preview text-preview", style: `border-color: ${options.value}; color: ${options.value}`, }); return makeElement( "label", { class: "color" }, textInput, backgroundPreviewNode, textPreviewNode ); } function makeNumericInput(options) { options.type = "number"; return makeInput(options); } function makeDateInput(options) { options.type = "date"; return makeInput(options); } function getPostUrl(id, parameters) { return uri.formatClientLink( "post", id, parameters ? { query: parameters.query } : {} ); } function getPostEditUrl(id, parameters) { return uri.formatClientLink( "post", id, "edit", parameters ? { query: parameters.query } : {} ); } function makePostLink(id, includeHash) { let text = id; if (includeHash) { text = "@" + id; } return api.hasPrivilege("posts:view") ? makeElement( "a", { href: uri.formatClientLink("post", id) }, misc.escapeHtml(text) ) : misc.escapeHtml(text); } function makeTagLink(name, includeHash, includeCount, tag) { const category = tag && tag.category ? tag.category : "unknown"; let text = misc.getPrettyName(name); if (includeHash === true) { text = "#" + text; } if (includeCount === true) { text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")"; } return api.hasPrivilege("tags:view") ? makeElement( "a", { href: uri.formatClientLink("tag", name), class: misc.makeCssName(category, "tag"), }, misc.escapeHtml(text) ) : makeElement( "span", { class: misc.makeCssName(category, "tag") }, misc.escapeHtml(text) ); } function makePoolLink(id, includeHash, includeCount, pool, name) { const category = pool && pool.category ? pool.category : "unknown"; let text = misc.getPrettyName( name ? name : pool && pool.names ? pool.names[0] : "pool " + id ); if (includeHash === true) { text = "#" + text; } if (includeCount === true) { text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")"; } return api.hasPrivilege("pools:view") ? makeElement( "a", { href: uri.formatClientLink("pool", id), class: misc.makeCssName(category, "pool"), }, misc.escapeHtml(text) ) : makeElement( "span", { class: misc.makeCssName(category, "pool") }, misc.escapeHtml(text) ); } function makeUserLink(user) { let text = makeThumbnail(user ? user.avatarUrl : null); text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous"; const link = user && user.name && api.hasPrivilege("users:view") ? makeElement( "a", { href: uri.formatClientLink("user", user.name) }, text ) : text; return makeElement("span", { class: "user" }, link); } function makeFlexboxAlign(options) { return [...misc.range(20)] .map(() => '
  • ') .join(""); } function makeAccessKey(html, key) { const regex = new RegExp("(" + key + ")", "i"); html = html.replace( regex, '$1' ); return html; } function _serializeElement(name, attributes) { return [name] .concat( Object.keys(attributes).map((key) => { if (attributes[key] === true) { return key; } else if ( attributes[key] === false || attributes[key] === undefined ) { return ""; } const attribute = misc.escapeHtml(attributes[key] || ""); return `${key}="${attribute}"`; }) ) .join(" "); } function makeElement(name, attrs, ...content) { return content.length !== undefined ? `<${_serializeElement(name, attrs)}>${content.join("")}` : `<${_serializeElement(name, attrs)}/>`; } function emptyContent(target) { while (target.lastChild) { target.removeChild(target.lastChild); } } function replaceContent(target, source) { emptyContent(target); if (source instanceof NodeList) { for (let child of [...source]) { target.appendChild(child); } } else if (source instanceof Node) { target.appendChild(source); } else if (source !== null) { throw `Invalid view source: ${source}`; } } function showMessage(target, message, className) { if (!message) { message = "Unknown message"; } const messagesHolderNode = target.querySelector(".messages"); if (!messagesHolderNode) { return false; } const textNode = document.createElement("div"); textNode.innerHTML = message.replace(/\n/g, "
    "); textNode.classList.add("message"); textNode.classList.add(className); const wrapperNode = document.createElement("div"); wrapperNode.classList.add("message-wrapper"); wrapperNode.appendChild(textNode); messagesHolderNode.appendChild(wrapperNode); return true; } function appendExclamationMark() { if (!document.title.startsWith("!")) { document.oldTitle = document.title; document.title = `! ${document.title}`; } } function showError(target, message) { appendExclamationMark(); return showMessage(target, misc.formatInlineMarkdown(message), "error"); } function showSuccess(target, message) { return showMessage(target, misc.formatInlineMarkdown(message), "success"); } function showInfo(target, message) { return showMessage(target, misc.formatInlineMarkdown(message), "info"); } function clearMessages(target) { if (document.oldTitle) { document.title = document.oldTitle; document.oldTitle = null; } for (let messagesHolderNode of target.querySelectorAll(".messages")) { emptyContent(messagesHolderNode); } } function htmlToDom(html) { // code taken from jQuery + Krasimir Tsonev's blog const wrapMap = { _: [1, "
    ", "
    "], option: [1, ""], legend: [1, "
    ", "
    "], area: [1, "", ""], param: [1, "", ""], thead: [1, "", "
    "], tr: [2, "", "
    "], td: [3, "", "
    "], col: [2, "", "
    "], }; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.thead; wrapMap.tfoot = wrapMap.thead; wrapMap.colgroup = wrapMap.thead; wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; let element = document.createElement("div"); const match = /<\s*(\w+)[^>]*?>/g.exec(html); if (match) { const tag = match[1]; const [depthToChild, prefix, suffix] = wrapMap[tag] || wrapMap._; element.innerHTML = prefix + html + suffix; for (let i = 0; i < depthToChild; i++) { element = element.lastChild; } } else { element.innerHTML = html; } return element.childNodes.length > 1 ? element.childNodes : element.firstChild; } function getTemplate(templatePath) { if (!(templatePath in templates)) { throw `Missing template: ${templatePath}`; } const templateFactory = templates[templatePath]; return (ctx) => { if (!ctx) { ctx = {}; } Object.assign(ctx, { getPostUrl: getPostUrl, getPostEditUrl: getPostEditUrl, makeRelativeTime: makeRelativeTime, makeFileSize: makeFileSize, makeMarkdown: makeMarkdown, makeThumbnail: makeThumbnail, makeRadio: makeRadio, makeCheckbox: makeCheckbox, makeSelect: makeSelect, makeInput: makeInput, makeButton: makeButton, makeTextarea: makeTextarea, makeTextInput: makeTextInput, makePasswordInput: makePasswordInput, makeEmailInput: makeEmailInput, makeColorInput: makeColorInput, makeDateInput: makeDateInput, makePostLink: makePostLink, makeTagLink: makeTagLink, makePoolLink: makePoolLink, makeUserLink: makeUserLink, makeFlexboxAlign: makeFlexboxAlign, makeAccessKey: makeAccessKey, makeElement: makeElement, makeCssName: misc.makeCssName, makeNumericInput: makeNumericInput, formatClientLink: uri.formatClientLink, }); return htmlToDom(templateFactory(ctx)); }; } function decorateValidator(form) { // postpone showing form fields validity until user actually tries // to submit it (seeing red/green form w/o doing anything breaks POLA) let submitButton = form.querySelector(".buttons input"); if (!submitButton) { submitButton = form.querySelector("input[type=submit]"); } if (submitButton) { submitButton.addEventListener("click", (e) => { form.classList.add("show-validation"); }); } form.addEventListener("submit", (e) => { form.classList.remove("show-validation"); }); } function disableForm(form) { for (let input of form.querySelectorAll("input")) { input.disabled = true; } } function enableForm(form) { for (let input of form.querySelectorAll("input")) { input.disabled = false; } } function syncScrollPosition() { window.requestAnimationFrame(() => { if ( history.state && Object.prototype.hasOwnProperty.call(history.state, "scrollX") ) { window.scrollTo(history.state.scrollX, history.state.scrollY); } else { window.scrollTo(0, 0); } }); } function slideDown(element) { const duration = 500; return new Promise((resolve, reject) => { const height = element.getBoundingClientRect().height; element.style.maxHeight = "0"; element.style.overflow = "hidden"; window.setTimeout(() => { element.style.transition = `all ${duration}ms ease`; element.style.maxHeight = `${height}px`; }, 50); window.setTimeout(() => { resolve(); }, duration); }); } function slideUp(element) { const duration = 500; return new Promise((resolve, reject) => { const height = element.getBoundingClientRect().height; element.style.overflow = "hidden"; element.style.maxHeight = `${height}px`; element.style.transition = `all ${duration}ms ease`; window.setTimeout(() => { element.style.maxHeight = 0; }, 10); window.setTimeout(() => { resolve(); }, duration); }); } function monitorNodeRemoval(monitoredNode, callback) { const mutationObserver = new MutationObserver((mutations) => { for (let mutation of mutations) { for (let node of mutation.removedNodes) { if (node.contains(monitoredNode)) { mutationObserver.disconnect(); callback(); return; } } } }); mutationObserver.observe(document.body, { childList: true, subtree: true, }); } document.addEventListener("input", (e) => { if (e.target.classList.contains("color")) { let bkNode = e.target.parentNode.querySelector(".background-preview"); let textNode = e.target.parentNode.querySelector(".text-preview"); bkNode.style.backgroundColor = e.target.value; bkNode.style.borderColor = e.target.value; textNode.style.color = e.target.value; textNode.style.borderColor = e.target.value; } }); // prevent opening buttons in new tabs document.addEventListener("click", (e) => { if (e.target.getAttribute("href") === "" && e.which === 2) { e.preventDefault(); } }); module.exports = { htmlToDom: htmlToDom, getTemplate: getTemplate, emptyContent: emptyContent, replaceContent: replaceContent, enableForm: enableForm, disableForm: disableForm, decorateValidator: decorateValidator, makeTagLink: makeTagLink, makePostLink: makePostLink, makePoolLink: makePoolLink, makeCheckbox: makeCheckbox, makeRadio: makeRadio, syncScrollPosition: syncScrollPosition, slideDown: slideDown, slideUp: slideUp, monitorNodeRemoval: monitorNodeRemoval, clearMessages: clearMessages, appendExclamationMark: appendExclamationMark, showError: showError, showSuccess: showSuccess, showInfo: showInfo, };