2016-04-09 18:54:23 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
require('../util/polyfill.js');
|
2016-05-09 20:07:54 +02:00
|
|
|
const underscore = require('underscore');
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
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(\'{0}\')'.format(url)
|
|
|
|
},
|
|
|
|
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-05-10 14:13:24 +02:00
|
|
|
return _makeLabel(options) +
|
|
|
|
makeVoidElement(
|
2016-05-09 20:07:54 +02:00
|
|
|
'input', {
|
|
|
|
type: options.inputType,
|
|
|
|
name: options.name,
|
|
|
|
id: options.id,
|
|
|
|
value: options.value || '',
|
|
|
|
required: options.required,
|
|
|
|
pattern: options.pattern,
|
|
|
|
placeholder: options.placeholder,
|
2016-05-11 23:46:56 +02:00
|
|
|
readonly: options.readonly,
|
2016-05-09 20:07:54 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeTextInput(options) {
|
|
|
|
options.inputType = 'text';
|
|
|
|
return makeInput(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makePasswordInput(options) {
|
|
|
|
options.inputType = 'password';
|
|
|
|
return makeInput(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeEmailInput(options) {
|
|
|
|
options.inputType = '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 makeTagLink(name) {
|
|
|
|
const tagExport = tags.getExport();
|
|
|
|
let category = null;
|
|
|
|
try {
|
2016-05-11 23:46:56 +02:00
|
|
|
category = tagExport.tags.get(name).category;
|
2016-05-10 10:57:59 +02:00
|
|
|
} catch (e) {
|
|
|
|
category = 'unknown';
|
|
|
|
}
|
|
|
|
return makeNonVoidElement('a', {
|
|
|
|
'href': '/tag/' + name,
|
|
|
|
'class': 'tag-' + category,
|
|
|
|
}, name);
|
|
|
|
}
|
|
|
|
|
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-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 '';
|
|
|
|
}
|
|
|
|
return '{0}="{1}"'.format(key, attributes[key]);
|
|
|
|
}))
|
|
|
|
.join(' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeNonVoidElement(name, attributes, content) {
|
2016-04-11 18:36:27 +02:00
|
|
|
return '<{0}>{1}</{2}>'.format(
|
|
|
|
_serializeElement(name, attributes), content, name);
|
2016-04-10 10:23:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function makeVoidElement(name, attributes) {
|
|
|
|
return '<{0}/>'.format(_serializeElement(name, attributes));
|
|
|
|
}
|
|
|
|
|
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-04-09 18:54:23 +02:00
|
|
|
console.error('Missing template: ' + templatePath);
|
|
|
|
return null;
|
|
|
|
}
|
2016-04-13 18:58:34 +02:00
|
|
|
const templateText = templates[templatePath].trim();
|
2016-05-09 20:07:54 +02:00
|
|
|
const templateFactory = underscore.template(templateText);
|
|
|
|
return ctx => {
|
|
|
|
if (!ctx) {
|
|
|
|
ctx = {};
|
|
|
|
}
|
|
|
|
underscore.extend(ctx, {
|
|
|
|
makeRelativeTime: makeRelativeTime,
|
|
|
|
makeThumbnail: makeThumbnail,
|
|
|
|
makeRadio: makeRadio,
|
|
|
|
makeCheckbox: makeCheckbox,
|
|
|
|
makeSelect: makeSelect,
|
|
|
|
makeInput: makeInput,
|
|
|
|
makeTextInput: makeTextInput,
|
|
|
|
makePasswordInput: makePasswordInput,
|
|
|
|
makeEmailInput: makeEmailInput,
|
2016-05-10 10:57:59 +02:00
|
|
|
makeColorInput: makeColorInput,
|
|
|
|
makeTagLink: makeTagLink,
|
2016-05-09 20:07:54 +02:00
|
|
|
makeFlexboxAlign: makeFlexboxAlign,
|
|
|
|
});
|
|
|
|
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);
|
|
|
|
} else {
|
|
|
|
console.error('Invalid view source', source);
|
|
|
|
}
|
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-10 10:57:59 +02:00
|
|
|
document.addEventListener('input', e => {
|
|
|
|
if (e.target.getAttribute('type').toLowerCase() === 'color') {
|
|
|
|
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-04-09 18:54:23 +02:00
|
|
|
};
|