2016-04-09 18:54:23 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
require('../util/polyfill.js');
|
2016-06-11 09:59:29 +02:00
|
|
|
const api = require('../api.js');
|
2016-05-21 09:46:41 +02:00
|
|
|
const templates = require('../templates.js');
|
2016-05-10 10:57:59 +02:00
|
|
|
const tags = require('../tags.js');
|
2016-04-09 18:54:23 +02:00
|
|
|
const events = require('../events.js');
|
|
|
|
const domParser = new DOMParser();
|
2016-05-09 20:07:54 +02:00
|
|
|
const misc = require('./misc.js');
|
|
|
|
|
2016-05-11 23:46:56 +02:00
|
|
|
function _imbueId(options) {
|
|
|
|
if (!options.id) {
|
|
|
|
options.id = 'gen-' + Math.random().toString(36).substring(7);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-09 20:07:54 +02:00
|
|
|
function _makeLabel(options, attrs) {
|
|
|
|
if (!options.text) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
if (!attrs) {
|
|
|
|
attrs = {};
|
|
|
|
}
|
|
|
|
attrs.for = options.id;
|
|
|
|
return makeNonVoidElement('label', attrs, options.text);
|
|
|
|
}
|
|
|
|
|
2016-05-22 22:39:31 +02:00
|
|
|
function makeFileSize(fileSize) {
|
|
|
|
return misc.formatFileSize(fileSize);
|
|
|
|
}
|
|
|
|
|
2016-05-09 20:07:54 +02:00
|
|
|
function makeRelativeTime(time) {
|
|
|
|
return makeNonVoidElement(
|
|
|
|
'time',
|
|
|
|
{datetime: time, title: time},
|
|
|
|
misc.formatRelativeTime(time));
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeThumbnail(url) {
|
|
|
|
return makeNonVoidElement(
|
|
|
|
'span',
|
|
|
|
{
|
|
|
|
class: 'thumbnail',
|
2016-05-20 23:59:24 +02:00
|
|
|
style: `background-image: url(\'${url}\')`,
|
2016-05-09 20:07:54 +02:00
|
|
|
},
|
|
|
|
makeVoidElement('img', {alt: 'thumbnail', src: url}));
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeRadio(options) {
|
2016-05-11 23:46:56 +02:00
|
|
|
_imbueId(options);
|
2016-05-09 20:07:54 +02:00
|
|
|
return makeVoidElement(
|
2016-05-11 21:29:57 +02:00
|
|
|
'input',
|
|
|
|
{
|
|
|
|
id: options.id,
|
|
|
|
name: options.name,
|
|
|
|
value: options.value,
|
|
|
|
type: 'radio',
|
|
|
|
checked: options.selectedValue === options.value,
|
|
|
|
required: options.required,
|
|
|
|
}) +
|
|
|
|
_makeLabel(options, {class: 'radio'});
|
2016-05-09 20:07:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function makeCheckbox(options) {
|
2016-05-11 23:46:56 +02:00
|
|
|
_imbueId(options);
|
2016-05-09 20:07:54 +02:00
|
|
|
return makeVoidElement(
|
2016-05-11 21:29:57 +02:00
|
|
|
'input',
|
|
|
|
{
|
|
|
|
id: options.id,
|
|
|
|
name: options.name,
|
|
|
|
value: options.value,
|
|
|
|
type: 'checkbox',
|
|
|
|
checked: options.checked !== undefined ?
|
|
|
|
options.checked : false,
|
|
|
|
required: options.required,
|
|
|
|
}) +
|
|
|
|
_makeLabel(options, {class: 'checkbox'});
|
2016-05-09 20:07:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function makeSelect(options) {
|
|
|
|
return _makeLabel(options) +
|
|
|
|
makeNonVoidElement(
|
|
|
|
'select',
|
2016-05-11 23:46:56 +02:00
|
|
|
{
|
|
|
|
id: options.id,
|
|
|
|
name: options.name,
|
|
|
|
disabled: options.readonly,
|
|
|
|
},
|
2016-05-09 20:07:54 +02:00
|
|
|
Object.keys(options.keyValues).map(key => {
|
|
|
|
return makeNonVoidElement(
|
|
|
|
'option',
|
|
|
|
{value: key, selected: key === options.selectedKey},
|
|
|
|
options.keyValues[key]);
|
|
|
|
}).join(''));
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeInput(options) {
|
2016-06-08 22:51:15 +02:00
|
|
|
options.value = options.value || '';
|
|
|
|
return _makeLabel(options) + makeVoidElement('input', options);
|
2016-05-09 20:07:54 +02:00
|
|
|
}
|
|
|
|
|
2016-06-01 21:31:15 +02:00
|
|
|
function makeButton(options) {
|
2016-06-08 22:51:15 +02:00
|
|
|
options.type = 'button';
|
2016-06-01 21:31:15 +02:00
|
|
|
return makeInput(options);
|
|
|
|
}
|
|
|
|
|
2016-05-09 20:07:54 +02:00
|
|
|
function makeTextInput(options) {
|
2016-06-08 22:51:15 +02:00
|
|
|
options.type = 'text';
|
2016-05-09 20:07:54 +02:00
|
|
|
return makeInput(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makePasswordInput(options) {
|
2016-06-08 22:51:15 +02:00
|
|
|
options.type = 'password';
|
2016-05-09 20:07:54 +02:00
|
|
|
return makeInput(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeEmailInput(options) {
|
2016-06-08 22:51:15 +02:00
|
|
|
options.type = 'email';
|
2016-05-09 20:07:54 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2016-05-29 12:28:52 +02:00
|
|
|
function makePostLink(id) {
|
2016-06-11 09:59:29 +02:00
|
|
|
const text = '@' + id;
|
|
|
|
return api.hasPrivilege('posts:view') ?
|
|
|
|
makeNonVoidElement('a', {'href': '/post/' + id}, text) :
|
|
|
|
text;
|
2016-05-29 12:28:52 +02:00
|
|
|
}
|
|
|
|
|
2016-05-10 10:57:59 +02:00
|
|
|
function makeTagLink(name) {
|
2016-05-29 12:28:52 +02:00
|
|
|
const tag = tags.getTagByName(name);
|
2016-06-11 09:59:29 +02:00
|
|
|
const category = tag ? tag.category : 'unknown';
|
|
|
|
return api.hasPrivilege('tags:view') ?
|
|
|
|
makeNonVoidElement(
|
|
|
|
'a', {
|
|
|
|
'href': '/tag/' + name,
|
|
|
|
'class': 'tag-' + category,
|
|
|
|
}, name) :
|
|
|
|
makeNonVoidElement(
|
|
|
|
'span', {
|
|
|
|
'class': 'tag-' + category,
|
|
|
|
},
|
|
|
|
name);
|
2016-05-10 10:57:59 +02:00
|
|
|
}
|
|
|
|
|
2016-05-29 12:28:52 +02:00
|
|
|
function makeUserLink(user) {
|
2016-06-11 11:00:52 +02:00
|
|
|
const text = makeThumbnail(user.avatarUrl) + user.name;
|
|
|
|
const link = api.hasPrivilege('users:view') ?
|
|
|
|
makeNonVoidElement('a', {'href': '/user/' + user.name}, text) :
|
|
|
|
text;
|
|
|
|
return makeNonVoidElement('span', {class: 'user'}, link);
|
2016-05-29 12:28:52 +02:00
|
|
|
}
|
|
|
|
|
2016-05-09 20:07:54 +02:00
|
|
|
function makeFlexboxAlign(options) {
|
|
|
|
return Array.from(misc.range(20))
|
|
|
|
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
|
|
|
}
|
2016-04-09 18:54:23 +02:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-04-10 10:23:27 +02:00
|
|
|
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 '';
|
|
|
|
}
|
2016-05-20 23:59:24 +02:00
|
|
|
return `${key}="${attributes[key]}"`;
|
2016-04-10 10:23:27 +02:00
|
|
|
}))
|
|
|
|
.join(' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeNonVoidElement(name, attributes, content) {
|
2016-05-20 23:59:24 +02:00
|
|
|
return `<${_serializeElement(name, attributes)}>${content}</${name}>`;
|
2016-04-10 10:23:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function makeVoidElement(name, attributes) {
|
2016-05-20 23:59:24 +02:00
|
|
|
return `<${_serializeElement(name, attributes)}/>`;
|
2016-04-10 10:23:27 +02:00
|
|
|
}
|
|
|
|
|
2016-05-11 21:29:57 +02:00
|
|
|
function _messageHandler(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 unlistenToMessages() {
|
2016-04-09 18:54:23 +02:00
|
|
|
events.unlisten(events.Success);
|
|
|
|
events.unlisten(events.Error);
|
2016-04-12 18:17:46 +02:00
|
|
|
events.unlisten(events.Info);
|
2016-05-11 21:29:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function listenToMessages(target) {
|
|
|
|
unlistenToMessages();
|
|
|
|
const listen = (eventType, className) => {
|
|
|
|
events.listen(
|
|
|
|
eventType,
|
|
|
|
msg => {
|
|
|
|
return _messageHandler(target, msg, className);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
listen(events.Success, 'success');
|
|
|
|
listen(events.Error, 'error');
|
|
|
|
listen(events.Info, 'info');
|
2016-04-09 18:54:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function clearMessages(target) {
|
|
|
|
const messagesHolder = target.querySelector('.messages');
|
|
|
|
/* TODO: animate that */
|
|
|
|
while (messagesHolder.lastChild) {
|
|
|
|
messagesHolder.removeChild(messagesHolder.lastChild);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function htmlToDom(html) {
|
|
|
|
const parsed = domParser.parseFromString(html, 'text/html').body;
|
|
|
|
return parsed.childNodes.length > 1 ?
|
|
|
|
parsed.childNodes :
|
|
|
|
parsed.firstChild;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTemplate(templatePath) {
|
2016-04-13 18:58:34 +02:00
|
|
|
if (!(templatePath in templates)) {
|
2016-06-11 09:30:22 +02:00
|
|
|
throw `Missing template: ${templatePath}`;
|
2016-04-09 18:54:23 +02:00
|
|
|
}
|
2016-05-21 09:46:41 +02:00
|
|
|
const templateFactory = templates[templatePath];
|
2016-05-09 20:07:54 +02:00
|
|
|
return ctx => {
|
|
|
|
if (!ctx) {
|
|
|
|
ctx = {};
|
|
|
|
}
|
2016-05-21 08:07:16 +02:00
|
|
|
Object.assign(ctx, {
|
2016-05-09 20:07:54 +02:00
|
|
|
makeRelativeTime: makeRelativeTime,
|
2016-05-22 22:39:31 +02:00
|
|
|
makeFileSize: makeFileSize,
|
2016-05-09 20:07:54 +02:00
|
|
|
makeThumbnail: makeThumbnail,
|
|
|
|
makeRadio: makeRadio,
|
|
|
|
makeCheckbox: makeCheckbox,
|
|
|
|
makeSelect: makeSelect,
|
|
|
|
makeInput: makeInput,
|
2016-06-01 21:31:15 +02:00
|
|
|
makeButton: makeButton,
|
2016-05-09 20:07:54 +02:00
|
|
|
makeTextInput: makeTextInput,
|
|
|
|
makePasswordInput: makePasswordInput,
|
|
|
|
makeEmailInput: makeEmailInput,
|
2016-05-10 10:57:59 +02:00
|
|
|
makeColorInput: makeColorInput,
|
2016-05-29 12:28:52 +02:00
|
|
|
makePostLink: makePostLink,
|
2016-05-10 10:57:59 +02:00
|
|
|
makeTagLink: makeTagLink,
|
2016-05-29 12:28:52 +02:00
|
|
|
makeUserLink: makeUserLink,
|
2016-05-09 20:07:54 +02:00
|
|
|
makeFlexboxAlign: makeFlexboxAlign,
|
2016-05-29 12:24:48 +02:00
|
|
|
makeAccessKey: makeAccessKey,
|
2016-05-09 20:07:54 +02:00
|
|
|
});
|
|
|
|
return htmlToDom(templateFactory(ctx));
|
2016-04-09 18:54:23 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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');
|
|
|
|
});
|
|
|
|
}
|
2016-04-09 18:54:23 +02:00
|
|
|
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 showView(target, source) {
|
2016-04-14 13:42:47 +02:00
|
|
|
while (target.lastChild) {
|
|
|
|
target.removeChild(target.lastChild);
|
|
|
|
}
|
|
|
|
if (source instanceof NodeList) {
|
|
|
|
for (let child of source) {
|
|
|
|
target.appendChild(child);
|
2016-04-09 18:54:23 +02:00
|
|
|
}
|
2016-04-14 13:42:47 +02:00
|
|
|
} else if (source instanceof Node) {
|
|
|
|
target.appendChild(source);
|
2016-06-11 09:30:22 +02:00
|
|
|
} else if (source !== null) {
|
|
|
|
throw `Invalid view source: ${source}`;
|
2016-04-14 13:42:47 +02:00
|
|
|
}
|
2016-04-09 18:54:23 +02:00
|
|
|
}
|
|
|
|
|
2016-04-14 19:37:05 +02:00
|
|
|
function scrollToHash() {
|
|
|
|
window.setTimeout(() => {
|
|
|
|
if (!window.location.hash) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const el = document.getElementById(
|
|
|
|
window.location.hash.replace(/#/, ''));
|
|
|
|
if (el) {
|
|
|
|
el.scrollIntoView();
|
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
}
|
|
|
|
|
2016-05-22 12:35:16 +02:00
|
|
|
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 => {
|
2016-05-16 23:01:12 +02:00
|
|
|
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-04-09 18:54:23 +02:00
|
|
|
module.exports = {
|
|
|
|
htmlToDom: htmlToDom,
|
|
|
|
getTemplate: getTemplate,
|
|
|
|
showView: showView,
|
|
|
|
enableForm: enableForm,
|
|
|
|
disableForm: disableForm,
|
|
|
|
listenToMessages: listenToMessages,
|
2016-05-11 21:29:57 +02:00
|
|
|
unlistenToMessages: unlistenToMessages,
|
2016-04-09 18:54:23 +02:00
|
|
|
clearMessages: clearMessages,
|
|
|
|
decorateValidator: decorateValidator,
|
2016-04-10 10:23:27 +02:00
|
|
|
makeVoidElement: makeVoidElement,
|
|
|
|
makeNonVoidElement: makeNonVoidElement,
|
2016-04-14 19:37:05 +02:00
|
|
|
scrollToHash: scrollToHash,
|
2016-05-22 12:35:16 +02:00
|
|
|
slideDown: slideDown,
|
|
|
|
slideUp: slideUp,
|
2016-05-29 12:22:59 +02:00
|
|
|
monitorNodeRemoval: monitorNodeRemoval,
|
2016-04-09 18:54:23 +02:00
|
|
|
};
|