676 lines
21 KiB
JavaScript
676 lines
21 KiB
JavaScript
class VUtils {
|
|
static makePublic() {
|
|
if (VUtils.isUsed) {
|
|
return;
|
|
}
|
|
this.initHandlers();
|
|
VUtils.isUsed = true;
|
|
console.log("[VUtils] is now available in the Global Space! no VUtils. anymore needed");
|
|
}
|
|
|
|
static initHandlers() {
|
|
window.$ = this.$;
|
|
window.$$ = this.$$;
|
|
window.tryCatch = this.tryCatch;
|
|
VUtils.nodePrototypes();
|
|
}
|
|
|
|
static $(selector, from) {
|
|
from = from || document;
|
|
return from.querySelector(selector);
|
|
}
|
|
|
|
static $$(selector, from) {
|
|
from = from || document;
|
|
return from.querySelectorAll(selector);
|
|
}
|
|
|
|
static tryCatch(data, callback, error) {
|
|
data = VUtils.wrap(data, []);
|
|
error = error || console.error;
|
|
callback = callback || console.log;
|
|
try {
|
|
callback(...data);
|
|
} catch (e) {
|
|
error(e);
|
|
}
|
|
}
|
|
|
|
static forEach(items, cb, error) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
VUtils.tryCatch([items[i], i], cb, error);
|
|
}
|
|
}
|
|
|
|
static get(valueOne, value) {
|
|
return this.wrap(valueOne, value);
|
|
}
|
|
|
|
static mergeKeys(root, target) {
|
|
root = root || {};
|
|
let keys = Object.keys(root);
|
|
for (let key of keys) {
|
|
target[key] = root[key];
|
|
}
|
|
return target;
|
|
}
|
|
|
|
static mergeOptions(target, root) {
|
|
root = root || {};
|
|
let keys = Object.keys(root);
|
|
for (let key of keys) {
|
|
target[key] = VUtils.get(root[key], target[key]);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
static wrap(valueOne, valueTwo) {
|
|
let x = typeof valueTwo;
|
|
if (!(valueOne instanceof Array) && valueTwo instanceof Array) {
|
|
return [valueOne];
|
|
}
|
|
if (x === 'string' && valueOne instanceof Array) {
|
|
return valueOne.join(".");
|
|
}
|
|
return valueOne === undefined ? valueTwo : valueOne;
|
|
}
|
|
|
|
static tempId() {
|
|
return 'temp_' + Math.random().toString(36).substr(2, 16);
|
|
}
|
|
|
|
static nodePrototypes() {
|
|
Node.prototype.find = function (selector) {
|
|
return this.closest(selector);
|
|
};
|
|
Node.prototype.createNew = function (tag, options) {
|
|
let el = document.createElement(tag);
|
|
el.classList.add(...VUtils.get(options.classes, []));
|
|
el.id = VUtils.get(options.id, '');
|
|
el.innerHTML = VUtils.get(options.content, "");
|
|
VUtils.mergeKeys(options.dataset, el.dataset);
|
|
if (VUtils.get(options.append, true) === true) {
|
|
this.appendChild(el);
|
|
}
|
|
|
|
return el;
|
|
};
|
|
Node.prototype.addDelegatedEventListener = function (type, aim, callback, err) {
|
|
if (!callback || !type || !aim)
|
|
return;
|
|
this.addMultiListener(type, (event) => {
|
|
let target = event.target;
|
|
if (event.detail instanceof HTMLElement) {
|
|
target = event.detail;
|
|
}
|
|
if (target instanceof HTMLElement) {
|
|
if (target.matches(aim)) {
|
|
VUtils.tryCatch([event, target], callback, err);
|
|
} else {
|
|
const parent = target.find(aim);
|
|
if (parent) {
|
|
VUtils.tryCatch([event, parent], callback, err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
Node.prototype.addMultiListener = function (listener, cb, options = {}) {
|
|
let splits = listener.split(" ");
|
|
for (let split of splits) {
|
|
this.addEventListener(split, cb, options);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
VUtils.makePublic();
|
|
|
|
|
|
class VRipple {
|
|
constructor(options = {}) {
|
|
if (!VUtils.isUsed) {
|
|
throw "VRipply is only with Public VUtils usable!"
|
|
}
|
|
let self = this;
|
|
self.options = JSON.parse('{"classes":["btn-ripple__effect"],"target":"body","selector":".btn-ripple"}');
|
|
VUtils.mergeOptions(self.options, options);
|
|
if (self.options.selector.indexOf("#") > -1) {
|
|
throw "ID's are not allowed as selector!";
|
|
}
|
|
this.instanceCheck();
|
|
this.ripples = [];
|
|
requestAnimationFrame(this.initHandler.bind(this));
|
|
}
|
|
|
|
instanceCheck() {
|
|
let opts = this.options;
|
|
const rawKey = [opts.target, opts.selector, opts.classes.join(".")].join(" ");
|
|
VRipple.instances = VRipple.instances || {};
|
|
VRipple.instances[rawKey] = this;
|
|
}
|
|
|
|
initHandler() {
|
|
let self = this;
|
|
let selector = self.options.selector;
|
|
let target = $(self.options.target);
|
|
target.addDelegatedEventListener('mousedown touchstart', selector, (e, el) => {
|
|
let pos = e.touches ? e.touches[0] : e;
|
|
let parent = el.parentNode;
|
|
let circle = el.createNew('span', self.options);
|
|
let bounds = parent.getBoundingClientRect();
|
|
let x = pos.clientX - bounds.left;
|
|
let y = pos.clientY - bounds.top;
|
|
circle.style.top = y + 'px';
|
|
circle.style.left = x + 'px';
|
|
circle._mouseDown = true;
|
|
circle._animationEnded = false;
|
|
self.ripples.push(circle);
|
|
});
|
|
document.body.addDelegatedEventListener('animationend', '.' + VUtils.get(self.options.classes, ''), self.rippleEnd.bind(self))
|
|
if (!document.body._vRippleInit) {
|
|
document.body.addMultiListener('mouseup touchend mouseleave rippleClose', e => {
|
|
let keys = Object.keys(VRipple.instances);
|
|
for (let key of keys) {
|
|
for (let ripple of VRipple.instances[key].ripples) {
|
|
self.rippleEnd.bind(VRipple.instances[key])(e, ripple);
|
|
}
|
|
}
|
|
})
|
|
document.body._vRippleInit = true;
|
|
}
|
|
}
|
|
|
|
rippleEnd(ev, el) {
|
|
const parent = el.parentNode;
|
|
if (parent) {
|
|
if (ev.type === 'animationend') {
|
|
el._animationEnded = true;
|
|
} else {
|
|
el._mouseDown = false;
|
|
}
|
|
if (!el._mouseDown && el._animationEnded) {
|
|
if (el.classList.contains('to-remove')) {
|
|
el.parentNode.removeChild(el);
|
|
this.ripples.splice(this.ripples.indexOf(el), 1)
|
|
} else {
|
|
el.classList.add('to-remove');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const rippler = new VRipple();
|
|
|
|
|
|
|
|
(function () {
|
|
window._openVSelect = null;
|
|
|
|
requestAnimationFrame(e => {
|
|
document.body.addEventListener('click', ev => {
|
|
if (window._openVSelect && ev.target.closest('v-select') !== window._openVSelect) {
|
|
window._openVSelect.toggle(false);
|
|
}
|
|
})
|
|
})
|
|
|
|
class VSelectElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
let self = this;
|
|
self._in = this.attachInternals();
|
|
self._in.role = 'select';
|
|
self.setAttribute('tabindex', 0);
|
|
self.update();
|
|
}
|
|
|
|
static get formAssociated() {
|
|
return true;
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['required', 'validity'];
|
|
}
|
|
|
|
get required() {
|
|
return this.hasAttribute('required');
|
|
}
|
|
|
|
set required(flag) {
|
|
this.toggleAttribute('required', Boolean(flag));
|
|
}
|
|
|
|
get name() {
|
|
return this.getAttribute('name');
|
|
}
|
|
|
|
set name(val) {
|
|
this.toggleAttribute('name', val);
|
|
}
|
|
|
|
get form() {
|
|
return this._in.form;
|
|
}
|
|
|
|
get options() {
|
|
return $$('v-options v-option', this);
|
|
}
|
|
|
|
get selected() {
|
|
return $$('v-options v-option[selected]', this);
|
|
}
|
|
|
|
update() {
|
|
let selected = [],
|
|
lbl = $('v-label', this),
|
|
fd = new FormData();
|
|
this.selected.forEach(e => {
|
|
selected.push(e.innerText);
|
|
fd.append(this.name, e.value);
|
|
})
|
|
lbl.attributeChangedCallback('value', '', selected.join(", "));
|
|
if (this.required && selected.length === 0) {
|
|
this._in.setValidity({customError: true}, "Option is needed");
|
|
} else {
|
|
this._in.setValidity({});
|
|
}
|
|
this._in.setFormValue(fd);
|
|
}
|
|
|
|
checkValidity() {
|
|
return this._in.checkValidity();
|
|
}
|
|
|
|
reportValidity() {
|
|
return this._in.reportValidity();
|
|
}
|
|
|
|
toggle(open) {
|
|
if (window._openVSelect && open) {
|
|
window._openVSelect.toggleSelect(false);
|
|
}
|
|
const options = $('v-options', this);
|
|
if (!open || this.isOpen) {
|
|
options.style.maxHeight = '0';
|
|
window._openVSelect = false;
|
|
this.isOpen = false;
|
|
this.update();
|
|
} else {
|
|
options.focus();
|
|
let height = 0,
|
|
children = options.children;
|
|
for (let i = 0; i < children.length; i++) {
|
|
height += children[i].offsetHeight;
|
|
}
|
|
options.style.maxHeight = height + 'px';
|
|
window._openVSelect = this;
|
|
this.isOpen = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
class VSelectOptionElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._in = this.attachInternals();
|
|
this._in.role = 'option';
|
|
this.addEventListener('click', e => {
|
|
let parent = this.parentNode.parentNode,
|
|
select = !this.selected;
|
|
if (!parent.hasAttribute('multiple')) {
|
|
parent.toggle(false);
|
|
for (let item of parent.selected) {
|
|
if (item !== this) {
|
|
item.removeAttribute('selected');
|
|
}
|
|
}
|
|
}
|
|
if (!this.disabled) {
|
|
this.attributeChangedCallback('selected', false, select, true);
|
|
this.parentNode.parentNode.update();
|
|
}
|
|
});
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['selected', 'disabled', 'value'];
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue, force) {
|
|
if (name === 'selected' && this.hasAttribute('disabled')) {
|
|
this.removeAttribute(name);
|
|
return;
|
|
}
|
|
if (name === 'disabled' && newValue === true && this.hasAttribute('selected')) {
|
|
this.attributeChangedCallback('selected', false, false);
|
|
}
|
|
|
|
if (force) {
|
|
if (newValue) {
|
|
this.setAttribute(name, newValue);
|
|
} else {
|
|
this.removeAttribute(name);
|
|
}
|
|
}
|
|
this[name] = newValue;
|
|
}
|
|
}
|
|
|
|
class VLabel extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.empty = this.getAttribute('empty') || "";
|
|
this.innerHTML = this.getAttribute("value") || this.empty;
|
|
this.addEventListener('click', this.openPopUp.bind(this));
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['empty', 'value'];
|
|
}
|
|
|
|
openPopUp() {
|
|
this.parentNode.toggle(true);
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'value') {
|
|
this.innerHTML = newValue || this.empty;
|
|
}
|
|
this[name] = newValue;
|
|
}
|
|
}
|
|
|
|
customElements.define("v-label", VLabel);
|
|
customElements.define("v-option", VSelectOptionElement);
|
|
customElements.define("v-select", VSelectElement);
|
|
})();
|
|
|
|
|
|
|
|
class FormHandler {
|
|
constructor(selector, parent, cb, err) {
|
|
this.cb = cb || console.log;
|
|
this.err = err || console.err;
|
|
$(parent).addDelegatedEventListener('submit', selector, this.handleEvent.bind(this));
|
|
}
|
|
|
|
handleEvent(e, el) {
|
|
e.preventDefault();
|
|
if (el.checkValidity()) {
|
|
const url = el.action ?? '';
|
|
if (url === '') {
|
|
console.error("No URL Found on Form", el);
|
|
return;
|
|
}
|
|
fetch(el.action, {
|
|
method: el.method.toUpperCase(),
|
|
credentials: 'same-origin',
|
|
body: new FormData(el),
|
|
redirect: 'manual'
|
|
}).then(res => {
|
|
if (!res.ok) {
|
|
throw new Error('Network response errored');
|
|
}
|
|
return res.json()
|
|
}).then(ev => this.cb(ev, el)).catch(ev => this.err(ev, el));
|
|
} else {
|
|
VUtils.forEach($$('input', el), ele => {
|
|
if (!ele.checkValidity()) {
|
|
let parent = ele.parentNode;
|
|
parent.classList.remove('valid');
|
|
parent.classList.add('invalid');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
(function () {
|
|
class VButton extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
let self = this;
|
|
self.id = self.id || VUtils.tempId();
|
|
self.label = document.createElement('label');
|
|
self.input = document.createElement('input');
|
|
self.errorBox = document.createElement('span');
|
|
self.errorBox.classList.add('error');
|
|
self.errorBox.innerHTML = self.dataset.error;
|
|
self.label.setAttribute('for', self.id);
|
|
self.input.id = self.id;
|
|
self.input.type = self.getAttribute('type') || 'text';
|
|
self.label.innerHTML = self.dataset.label;
|
|
self.input.value = self.innerHTML.trim();
|
|
self.innerHTML = '';
|
|
self.input.required = self.hasAttribute('required');
|
|
self.input.name = self.getAttribute('name');
|
|
self.appendChild(self.input)
|
|
self.appendChild(self.label)
|
|
self.appendChild(self.errorBox);
|
|
self.input.addMultiListener('change input', self.cb.bind(self));
|
|
}
|
|
|
|
connectedCallback() {
|
|
let cl = this.classList;
|
|
if (this.input.value === "") {
|
|
cl.remove('focus')
|
|
} else {
|
|
cl.add('focus')
|
|
}
|
|
}
|
|
|
|
cb(e) {
|
|
let el = e.currentTarget
|
|
let errorMessage = $('.error-message', el.find('form'));
|
|
if (errorMessage) {
|
|
errorMessage.classList.add('hide')
|
|
}
|
|
let cl = this.classList;
|
|
if (el.value === "") {
|
|
cl.remove('focus')
|
|
} else {
|
|
cl.add('focus')
|
|
}
|
|
if (el.checkValidity()) {
|
|
cl.add('valid');
|
|
cl.remove('invalid');
|
|
} else {
|
|
cl.remove('valid');
|
|
cl.add('invalid');
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define("v-button", VButton);
|
|
|
|
if ($('#login')) {
|
|
new FormHandler('form#login', 'body', () => {
|
|
location.reload();
|
|
}, (e, el) => {
|
|
$('.error-message', el).classList.remove('hide');
|
|
})
|
|
}
|
|
})();
|
|
(() => {
|
|
class VEdit {
|
|
constructor(selector) {
|
|
let self = this;
|
|
self.editor = selector instanceof HTMLElement ? selector : $(selector);
|
|
self.todoOnKey = {};
|
|
self.keys = [];
|
|
self.backup = [];
|
|
self.taberr = [">", " ", "\n", "<"];
|
|
self.name = 'veditor-' + self.editor.id;
|
|
self.init();
|
|
self.selfClosing = ["img", "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta", "param", "source", "track"];
|
|
self.restore();
|
|
}
|
|
|
|
init() {
|
|
let self = this;
|
|
self.editor.addEventListener('keydown', self.handleKey.bind(self));
|
|
self.addKey('Tab', self.pressTab.bind(self));
|
|
self.addKey('<', self.addEmptyTags.bind(self));
|
|
self.addKey('ctrl-z', self.undo.bind(self));
|
|
self.addKey('ctrl-s', self.store.bind(self));
|
|
self.addKey('ctrl-shift-S', self.delete.bind(self));
|
|
self.editor.classList.add(self.name, 'veditor')
|
|
}
|
|
|
|
registerSelfClosing(name) {
|
|
this.selfClosing.push(name);
|
|
}
|
|
|
|
restore() {
|
|
let item = localStorage.getItem(this.name);
|
|
if (item) {
|
|
this.editor.value = item;
|
|
}
|
|
}
|
|
|
|
delete() {
|
|
localStorage.removeItem(this.name);
|
|
console.log(`[VEdit] Editor: ${this.name} removed`);
|
|
}
|
|
|
|
store() {
|
|
localStorage.setItem(this.name, this.editor.value);
|
|
console.log(`[VEdit] Editor: ${this.name} saved`);
|
|
}
|
|
|
|
handleKey(e) {
|
|
let self = this;
|
|
if ((e.ctrlKey && e.key === 'Control')
|
|
|| (e.shiftKey && e.key === 'Shift')) {
|
|
return;
|
|
}
|
|
let key;
|
|
if (e.ctrlKey && e.shiftKey) {
|
|
key = 'ctrl-shift-' + e.key;
|
|
} else if (e.ctrlKey) {
|
|
key = 'ctrl-' + e.key;
|
|
}
|
|
if (key) {
|
|
if (this.keys.indexOf(key) !== -1) {
|
|
e.preventDefault();
|
|
this.todoOnKey[key]();
|
|
return;
|
|
}
|
|
}
|
|
let cont = self.editor.value;
|
|
const pos = self.editor.selectionStart
|
|
if (self.backup.length > 50) {
|
|
self.backup.shift();
|
|
}
|
|
self.backup.push([cont, pos]);
|
|
if (self.keys.indexOf(e.key) > -1) {
|
|
e.preventDefault();
|
|
let w = self.todoOnKey[e.key](pos, cont, this.editor);
|
|
w[0].push(cont.substr(pos))
|
|
self.afterWork(w);
|
|
}
|
|
}
|
|
|
|
undo() {
|
|
let back = this.backup.pop() || [this.editor.value, this.editor.selectionStart];
|
|
this.editor.value = back[0];
|
|
this.editor.setSelectionRange(back[1], back[1]);
|
|
}
|
|
|
|
afterWork(data) {
|
|
this.setText(data[0].join(""));
|
|
this.editor.setSelectionRange(data[1], data[1]);
|
|
}
|
|
|
|
setText(text) {
|
|
this.editor.value = text;
|
|
}
|
|
|
|
addKey(name, func) {
|
|
this.todoOnKey[name] = func;
|
|
this.keys.push(name);
|
|
}
|
|
|
|
addEmptyTags(pos, cont, e) {
|
|
return [[cont.substr(0, pos), '<>'], pos + 1];
|
|
}
|
|
|
|
pressTab(pos, cont, e) {
|
|
let self = this;
|
|
let sub, prevContent, moveTo = pos;
|
|
if (pos === 0 || self.taberr.indexOf(cont[pos - 1]) !== -1) {
|
|
sub = ` `;
|
|
moveTo += 4;
|
|
prevContent = cont.substr(0, pos);
|
|
} else if (self.taberr.indexOf(cont[pos - 1]) === -1) {
|
|
let i = 2;
|
|
while (self.taberr.indexOf(cont[pos - i]) === -1 && pos - i > 0) {
|
|
i++;
|
|
}
|
|
if (pos - i > 0) {
|
|
i -= 1;
|
|
}
|
|
let gen = self.generateTag(cont.substr(pos - i, i).trim());
|
|
sub = gen[0];
|
|
moveTo = pos - i + gen[1];
|
|
prevContent = cont.substr(0, pos - i);
|
|
}
|
|
return [[prevContent, sub], moveTo]
|
|
}
|
|
|
|
generateTag(sub) {
|
|
let raw,
|
|
groups = {'.': [], '#': []},
|
|
keys = Object.keys(groups),
|
|
cGroup = 'cl',
|
|
split = sub.split(/([#.])/g);
|
|
raw = split.shift();
|
|
for (let item of split) {
|
|
if (keys.indexOf(item) > -1) {
|
|
cGroup = item;
|
|
continue;
|
|
}
|
|
groups[cGroup].push(item);
|
|
}
|
|
let second = '';
|
|
if (groups["."].length > 0) {
|
|
second += ` class="${groups["."].join(" ")}"`;
|
|
}
|
|
if (groups['#'].length > 0) {
|
|
second += ` id="${groups['#'].join("-")}"`;
|
|
}
|
|
const c = this.selfClosing;
|
|
let close = '';
|
|
if (c.indexOf(raw.trim()) === -1) {
|
|
close = `</${raw}>`;
|
|
}
|
|
let pre = `<${raw}${second}>`;
|
|
return [`${pre}${close}`, pre.length];
|
|
}
|
|
}
|
|
|
|
class VEditor extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.editor = document.createElement('textarea');
|
|
this.editor.innerHTML = this.innerHTML;
|
|
this.editor.id = this.getAttribute('name');
|
|
for (let attribute of this.attributes) {
|
|
this.editor.setAttribute(attribute.name, attribute.value);
|
|
}
|
|
this.innerHTML = '';
|
|
this.appendChild(this.editor);
|
|
this.edit = new VEdit(this.editor);
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.edit.restore();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.edit.save();
|
|
}
|
|
}
|
|
|
|
customElements.define("v-editor", VEditor);
|
|
})(); |