szurubooru/client/js/util/views.js

597 lines
17 KiB
JavaScript

"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(() => '<li class="flexbox-dummy"></li>')
.join("");
}
function makeAccessKey(html, key) {
const regex = new RegExp("(" + key + ")", "i");
html = html.replace(
regex,
'<span class="access-key" data-accesskey="$1">$1</span>'
);
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("")}</${name}>`
: `<${_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, "<br/>");
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, "<div>", "</div>"],
option: [1, "<select multiple>", "</select>"],
legend: [1, "<fieldset>", "</fieldset>"],
area: [1, "<map>", "</map>"],
param: [1, "<object>", "</object>"],
thead: [1, "<table>", "</table>"],
tr: [2, "<table><tbody>", "</tbody></table>"],
td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
};
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,
};