szurubooru/client/js/util/views.js

501 lines
14 KiB
JavaScript
Raw Normal View History

'use strict';
require('../util/polyfill.js');
2016-06-11 09:59:29 +02:00
const api = require('../api.js');
const templates = require('../templates.js');
2016-05-10 10:57:59 +02:00
const tags = require('../tags.js');
const domParser = new DOMParser();
const misc = require('./misc.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 makeNonVoidElement('label', attrs, options.text);
}
function makeFileSize(fileSize) {
return misc.formatFileSize(fileSize);
}
function makeMarkdown(text) {
return misc.formatMarkdown(text);
}
function makeRelativeTime(time) {
return makeNonVoidElement(
'time',
{datetime: time, title: time},
misc.formatRelativeTime(time));
}
function makeThumbnail(url) {
return makeNonVoidElement(
'span',
{
class: 'thumbnail',
style: `background-image: url(\'${url}\')`,
},
makeVoidElement('img', {alt: 'thumbnail', src: url}));
}
function makeRadio(options) {
_imbueId(options);
return makeVoidElement(
'input',
{
id: options.id,
name: options.name,
value: options.value,
type: 'radio',
checked: options.selectedValue === options.value,
disabled: options.readonly,
required: options.required,
}) +
_makeLabel(options, {class: 'radio'});
}
function makeCheckbox(options) {
_imbueId(options);
return makeVoidElement(
'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,
}) +
_makeLabel(options, {class: 'checkbox'});
}
function makeSelect(options) {
return _makeLabel(options) +
makeNonVoidElement(
'select',
{
id: options.id,
name: options.name,
disabled: options.readonly,
},
Object.keys(options.keyValues).map(key => {
return makeNonVoidElement(
'option',
{value: key, selected: key === options.selectedKey},
options.keyValues[key]);
}).join(''));
}
function makeInput(options) {
options.value = options.value || '';
return _makeLabel(options) + makeVoidElement('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) + makeNonVoidElement('textarea', options, value);
}
function makePasswordInput(options) {
options.type = 'password';
return makeInput(options);
}
function makeEmailInput(options) {
options.type = 'email';
return makeInput(options);
}
2016-05-10 10:57:59 +02:00
function makeColorInput(options) {
const textInput = makeVoidElement(
'input', {
type: 'text',
value: options.value || '',
required: options.required,
style: 'color: ' + options.value,
disabled: true,
});
const colorInput = makeVoidElement(
'input', {
type: 'color',
value: options.value || '',
});
2016-05-10 14:13:24 +02:00
return makeNonVoidElement(
'label', {class: 'color'}, colorInput + textInput);
2016-05-10 10:57:59 +02:00
}
function makeNumericInput(options) {
options.type = 'number';
return makeInput(options);
}
function getPostUrl(id, parameters) {
let url = '/post/' + encodeURIComponent(id);
if (parameters && parameters.query) {
url += '/query=' + encodeURIComponent(parameters.query);
}
return url;
}
function getPostEditUrl(id, parameters) {
let url = '/post/' + encodeURIComponent(id) + '/edit';
if (parameters && parameters.query) {
url += '/query=' + encodeURIComponent(parameters.query);
}
return url;
}
function makePostLink(id, includeHash) {
let text = id;
if (includeHash) {
text = '@' + id;
}
2016-06-11 09:59:29 +02:00
return api.hasPrivilege('posts:view') ?
makeNonVoidElement(
'a', {'href': '/post/' + encodeURIComponent(id)}, text) :
2016-06-11 09:59:29 +02:00
text;
}
function makeTagLink(name, includeHash) {
const tag = tags.getTagByName(name);
2016-06-11 09:59:29 +02:00
const category = tag ? tag.category : 'unknown';
let text = name;
if (includeHash === true) {
text = '#' + text;
}
2016-06-11 09:59:29 +02:00
return api.hasPrivilege('tags:view') ?
makeNonVoidElement(
'a',
{
'href': '/tag/' + encodeURIComponent(name),
'class': misc.makeCssName(category, 'tag'),
2016-06-11 09:59:29 +02:00
},
text) :
makeNonVoidElement(
'span',
{'class': misc.makeCssName(category, 'tag')},
text);
2016-05-10 10:57:59 +02:00
}
function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
text += user && user.name ? user.name : 'Anonymous';
const link = user && api.hasPrivilege('users:view') ?
makeNonVoidElement(
'a', {'href': '/user/' + encodeURIComponent(user.name)}, text) :
2016-06-11 11:00:52 +02:00
text;
return makeNonVoidElement('span', {class: 'user'}, link);
}
function makeFlexboxAlign(options) {
2016-08-05 20:09:11 +02:00
return [...misc.range(20)]
.map(() => '<li class="flexbox-dummy"></li>').join('');
}
2016-05-29 12:24:48 +02:00
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 makeNonVoidElement(name, attributes, content) {
return `<${_serializeElement(name, attributes)}>${content}</${name}>`;
}
function makeVoidElement(name, attributes) {
return `<${_serializeElement(name, attributes)}/>`;
}
function showMessage(target, message, className) {
if (!message) {
message = 'Unknown message';
}
const messagesHolder = target.querySelector('.messages');
if (!messagesHolder) {
return false;
}
/* TODO: animate this */
const node = document.createElement('div');
node.innerHTML = message.replace(/\n/g, '<br/>');
node.classList.add('message');
node.classList.add(className);
const wrapper = document.createElement('div');
wrapper.classList.add('message-wrapper');
wrapper.appendChild(node);
messagesHolder.appendChild(wrapper);
return true;
}
function showError(target, message) {
return showMessage(target, message, 'error');
}
function showSuccess(target, message) {
return showMessage(target, message, 'success');
}
function showInfo(target, message) {
return showMessage(target, message, 'info');
}
function clearMessages(target) {
const messagesHolder = target.querySelector('.messages');
/* TODO: animate that */
while (messagesHolder.lastChild) {
messagesHolder.removeChild(messagesHolder.lastChild);
}
}
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 = {};
}
2016-08-05 20:09:11 +02:00
Object.assign(ctx, misc.arrayToObject([
getPostUrl,
getPostEditUrl,
makeRelativeTime,
makeFileSize,
makeMarkdown,
makeThumbnail,
makeRadio,
makeCheckbox,
makeSelect,
makeInput,
makeButton,
makeTextarea,
makeTextInput,
makePasswordInput,
makeEmailInput,
makeColorInput,
makePostLink,
makeTagLink,
makeUserLink,
makeFlexboxAlign,
makeAccessKey,
misc.makeCssName,
makeNumericInput,
], func => func.name));
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)
2016-05-10 10:57:59 +02:00
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 replaceContent(target, source) {
2016-04-14 13:42:47 +02:00
while (target.lastChild) {
target.removeChild(target.lastChild);
}
if (source instanceof NodeList) {
2016-08-05 20:09:11 +02:00
for (let child of [...source]) {
2016-04-14 13:42:47 +02:00
target.appendChild(child);
}
2016-04-14 13:42:47 +02:00
} else if (source instanceof Node) {
target.appendChild(source);
} else if (source !== null) {
throw `Invalid view source: ${source}`;
2016-04-14 13:42:47 +02:00
}
}
2016-07-13 17:18:28 +02:00
function syncScrollPosition() {
window.requestAnimationFrame(
() => {
if (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);
});
}
2016-05-29 12:22:59 +02:00
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});
}
2016-05-10 10:57:59 +02:00
document.addEventListener('input', e => {
const type = e.target.getAttribute('type');
if (type && type.toLowerCase() === 'color') {
2016-05-10 10:57:59 +02:00
const textInput = e.target.parentNode.querySelector('input[type=text]');
textInput.style.color = e.target.value;
textInput.value = e.target.value;
}
});
2016-08-05 20:09:11 +02:00
module.exports = misc.arrayToObject([
htmlToDom,
getTemplate,
replaceContent,
enableForm,
disableForm,
decorateValidator,
makeVoidElement,
makeNonVoidElement,
makeTagLink,
makePostLink,
2016-08-20 22:40:25 +02:00
makeCheckbox,
makeRadio,
2016-08-05 20:09:11 +02:00
syncScrollPosition,
slideDown,
slideUp,
monitorNodeRemoval,
clearMessages,
showError,
showSuccess,
showInfo,
], func => func.name);