szurubooru/client/js/util/views.js
2020-05-03 19:53:28 -07:00

568 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 : 'unknown';
let text = misc.getPrettyTagName(name);
if (includeHash === true) {
text = '#' + text;
}
if (includeCount === true) {
text += ' (' + (tag ? 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(pool, includeHash, includeCount, name) {
const category = pool.category;
let text = name ? name : pool.names[0];
if (includeHash === true) {
text = '#' + text;
}
if (includeCount === true) {
text += ' (' + pool.postCount + ')';
}
return api.hasPrivilege('pools:view') ?
makeElement(
'a',
{
href: uri.formatClientLink('pool', 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 && 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.tfoot =
wrapMap.colgroup =
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 && history.state.hasOwnProperty('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,
};