// -*- coding: utf-8; -*-
// Package WCom.Modal
WCom.Modal = (function() {
const keyCodes = { enter: 13, escape: 27 };
const modalList = (() => {
let modals = [];
return {
add(id) {
if (modals.indexOf(id) === -1) modals.push(id);
},
isTopModal(id) {
return modals[modals.length - 1] === id;
},
remove(id) {
modals = modals.filter(m => m !== id);
}
}
})();
class Backdrop {
constructor(options) {
this.popupBackground = null;
this.popupContainer = null;
if (options) this.zIndex = opts.zIndex || null;
}
add(el) {
this.popupContainer = this.h.div({
className: 'modal-container out'
}, el);
this.popupBackground = this.h.div({
className: 'modal-outer-wrapper',
style: this.zIndex ? `z-index: ${this.zIndex}` : ''
}, this.popupContainer);
document.body.appendChild(this.popupBackground);
setTimeout(() => {
this.popupContainer.classList.add('in');
this.popupContainer.classList.remove('out');
}, 50);
}
remove(el) {
if (!el) return;
const popupParent = this.popupBackground.parentNode;
if (popupParent) popupParent.removeChild(this.popupBackground);
const elParent = el.parentNode;
elParent.classList.add('out');
elParent.classList.remove('in');
}
}
Object.assign(Backdrop.prototype, WCom.Util.Markup);
class Button {
buttonConfig;
constructor(args = {}) {
const isButton = !args.url;
const type = isButton ? 'button' : 'a';
let classes = 'button';
if (args.modifiers) {
classes += args.modifiers.map(m => ` button-${m}`).join('');
}
const attrs = { className: classes };
['id', 'onclick', 'title', 'type'].forEach((a) => {
if (args[a]) attrs[a] = args[a];
});
if (args.data) {
for (const a of Object.keys(args.data))
attrs[`data-${a}`] = args.data[a];
}
if (!isButton) attrs.href = args.url;
this.text = document.createTextNode(args.text || '');
this.elm = this.h[type](attrs, this.h.span([this.text]));
if (args.parent) args.parent.appendChild(this.elm);
}
activate() {
this.elm.classList.add('button-active');
}
deactivate() {
this.elm.classList.remove('button-active');
}
disable() {
this.elm.disabled = true;
}
element() {
return this.elm;
}
enable() {
this.elm.disabled = false;
}
updateText(text) {
this.text.nodeValue = text;
}
}
Object.assign(Button.prototype, WCom.Util.Markup);
class Drag {
constructor(args) {
this.drag = {};
this.dragNodeX = null;
this.dragNodeY = null;
this.scrollWrapper = document.querySelector(args.scrollWrapper);
}
autoScrollHandler(event) {
const { drag } = this;
const threshold = drag.autoScroll;
if (!threshold || threshold < 1) return;
const y = event.pageY;
const body = document.body;
const minY = body.scrollTop;
const maxY = minY + drag.viewportHeight;
let scrollDirection = 'noScroll';
if (y + threshold > maxY) scrollDirection = 'down';
if (y - threshold < minY) scrollDirection = 'up';
if (drag.scrollDirection !== scrollDirection) {
drag.scrollDirection = scrollDirection;
setScrollInterval();
}
}
clearScrollInterval() {
if (this.drag.scrollInterval) clearInterval(this.drag.scrollInterval);
}
dragHandler(event, options = {}) {
const { drag } = this;
if (drag.autoScroll) this.autoScrollHandler(event);
if (drag.moveCallback) drag.moveCallback(event, drag.dragNode);
if (drag.updateDropNodePositions) this.updateDropNodePositions();
this.dragNodeX = event.pageX;
this.dragNodeY = event.pageY;
if (drag.fixLeft) this.dragNodeX = drag.fixLeft;
const { constraints } = drag;
if (constraints) {
if (constraints.top)
this.dragNodeY = Math.max(this.dragNodeY, constraints.top);
if (constraints.bottom)
this.dragNodeY = Math.min(this.dragNodeY, constraints.bottom);
if (constraints.left)
this.dragNodeX = Math.max(this.dragNodeX, constraints.left);
if (constraints.right)
this.dragNodeX = Math.min(this.dragNodeX, constraints.right);
}
this.updateHoveredNode(event);
if (drag.dragNodeOffset) {
this.dragNodeX -= drag.dragNodeOffset.x;
this.dragNodeY -= drag.dragNodeOffset.y;
}
if (drag.dragNode) {
drag.dragNode.style.left = this.dragNodeX + 'px';
drag.dragNode.style.top = this.dragNodeY + 'px';
}
}
dropHandler(event) {
const { drag } = this;
if (drag.currentDropNode)
this.leaveHandler(event, drag.currentDropNode);
if (drag.dropCallback)
drag.dropCallback(drag.currentDropNode, drag.dragNode);
this.stopDrag();
}
hoverHandler(event, node) {
const { drag } = this;
if (drag.hoverClass) node.classList.add(drag.hoverClass);
if (drag.hoverCallback) drag.hoverCallback(node, drag.dragNode, true);
}
leaveHandler(event, node) {
const { drag } = this;
if (drag.hoverClass) node.classList.remove(drag.hoverClass);
if (drag.hoverCallback) drag.hoverCallback(node, drag.dragNode, false);
}
scrollHandler(event) {
this.updateDropNodePositions();
this.updateHoveredNode(event);
}
setScrollInterval() {
this.clearScrollInterval();
const { drag } = this;
if (drag.scrollDirection === 'noScroll') return;
const scrollByValue = drag.scrollDirection === 'down'
? drag.autoScrollStep : -drag.autoScrollStep;
drag.scrollInterval = setInterval(function() {
this.scrollWrapper.scrollBy(0, scrollByValue);
}.bind(this), drag.autoScrollSpeed);
}
start(event, options = {}) {
if (!event) throw new Error('Event not specified');
event.preventDefault();
this.stopDrag();
const autoScroll = options.autoScroll === true
? 80 : options.autoScroll || false;
this.drag = {
autoScroll: autoScroll,
autoScrollSpeed: options.autoScrollSpeed || 10,
autoScrollStep: options.autoScrollStep || 5,
constraints: options.constraints,
currentDropNode: null,
documentHeight: this.h.getDimensions(document).height,
dragNode: options.dragNode,
dragNodeOffset: options.dragNodeOffset,
dropCallback: options.dropCallback,
dropNodes: options.dropTargets || {},
fixLeft: options.fixLeft,
hoverCallback: options.hoverCallback,
hoverClass: options.hoverClass,
moveCallback: options.moveCallback,
viewportHeight: this.h.getDimensions(window).height
};
const { drag, scrollWrapper } = this;
if (options.offsetDragNode) {
const position = this.h.getOffset(event.target);
drag.dragNodeOffset = {
x: event.pageX - position.left,
y: event.pageY - position.top,
};
}
if (drag.dragNode) drag.dragNode.style.position = 'absolute';
document.addEventListener('mousemove', this.dragHandler.bind(this));
document.addEventListener('mouseup', this.dropHandler.bind(this));
document.addEventListener('wheel', this.wheelHandler.bind(this));
scrollWrapper.addEventListener('scroll', this.scrollHandler.bind(this));
this.updateDropNodePositions();
this.dragHandler(event);
}
state() {
return this.drag;
}
stopDrag() {
this.scrollWrapper.removeEventListener('scroll', this.scrollHandler);
document.removeEventListener('wheel', this.wheelHandler);
document.removeEventListener('mouseup', this.dropHandler);
document.removeEventListener('mousemove', this.dragHandler);
this.clearScrollInterval();
const { drag } = this;
if (drag.dragNode) {
drag.dragNode.style.left = null;
drag.dragNode.style.position = null;
drag.dragNode.style.top = null;
}
this.drag = {};
}
updateDropNodePositions() {
const { drag } = this;
drag.dropNodePositions = [];
drag.dropNodes.forEach(function(node) {
const offsets = this.h.getOffset(node);
const dimensions = this.h.getDimensions(node);
drag.dropNodePositions.push({
bottom: offsets.top + dimensions.height,
left: offsets.left,
node: node,
right: offsets.left + dimensions.width,
top: offsets.top
});
}.bind(this));
drag.updateDropNodePositions = false;
}
updateHoveredNode(event) {
let hoveredNode = null;
const { drag, dragNodeX, dragNodeY } = this;
drag.dropNodePositions ||= [];
for (const target of drag.dropNodePositions) {
if (dragNodeX > target.left
&& dragNodeX < target.right
&& dragNodeY > target.top
&& dragNodeY < target.bottom
&& target.node[0] != drag.dragNode[0]) {
hoveredNode = target;
break;
}
}
if (hoveredNode != drag.currentDropNode) {
if (drag.currentDropNode)
this.leaveHandler(event, drag.currentDropNode);
if (hoveredNode) this.hoverHandler(event, hoveredNode);
drag.currentDropNode = hoveredNode;
}
}
wheelHandler(event) {
this.scrollWrapper.scrollBy(0, Math.floor(event.deltaY / 7));
}
}
Object.assign(Drag.prototype, WCom.Util.Markup);
class Modal {
constructor(title, content, buttons, setup) {
this.buttonClass = setup.buttonClass;
this.buttons = buttons;
this.classList = setup.classList;
this.closeCallback = setup.closeCallback;
this.content = content;
this.dragScrollWrapper = setup.dragScrollWrapper || '.standard';
this.icons = setup.icons;
this.ident = this.guid();
this.open = true;
this.resizeElement = setup.resizeElement;
this.title = title;
modalList.add(this.ident);
this.keyHandler = this.keyHandler.bind(this);
window.addEventListener('keydown', this.keyHandler);
}
buttonHandler(buttonConfig) {
if (buttonConfig.onclick(this) !== false) this.close();
}
close() {
if (!this.open) return;
this.open = false;
modalList.remove(this.ident);
window.removeEventListener('keydown', this.keyHandler);
this.backdrop.remove(this.el);
this.backdrop = null;
if (this.closeCallback) this.closeCallback();
}
keyHandler(event) {
const { keyCode } = event;
if (!modalList.isTopModal(this.ident)) return;
const btn = this.buttons.find(b => b.key && keyCodes[b.key] === keyCode);
if (btn) this.buttonHandler(btn);
else if (keyCode === keyCodes['escape']) this.close();
}
render() {
const classes = this.classList || '';
this.el = this.h.div({ className: 'modal ' + classes });
this.modalHeader = this.h.div({
className: 'modal-header', onmousedown: this._clickHandler(this.el)
}, [
this.h.h1({ className: 'modal-title' }, this.title),
this._createCloseIcon()
]);
this.modalHeader.setAttribute('draggable', 'draggable');
this.el.appendChild(this.modalHeader);
this.content = this.h.div(
{ className: 'modal-content-wrapper' },
this.h.div({ className: 'modal-content' }, this.content)
);
this.el.appendChild(this.content);
if (this.buttons.length) this._renderButtons(this.el);
this.backdrop = new Backdrop();
this.backdrop.add(this.el);
}
_createCloseIcon() {
const attr = {
className: 'button-icon modal-close',
onclick: function(event) {
event.preventDefault();
this.close();
}.bind(this)
};
const icons = this.icons;
if (!icons) return this.h.span(attr, 'X');
return this.h.span(attr, this.h.icon({
className: 'close-icon', icons, name: 'close'
}));
}
_clickHandler(el) {
return function(event) {
if (event.target.tagName === 'BUTTON') return;
if (event.target.tagName === 'SPAN') return;
const { left, top } = this.modalHeader.getBoundingClientRect();
const { scrollTop } = document.documentElement || document.body;
const drag = new Drag({ scrollWrapper: this.dragScrollWrapper });
drag.start(event, {
dragNode: el,
dropTargets: [],
dragNodeOffset: {
x: event.clientX - left,
y: (event.clientY + scrollTop) - top
}
});
}.bind(this);
}
_renderButtons(el) {
this.buttonBox = this.h.div({ className: 'modal-footer' });
if (this.resizeElement) {
const resizeSouth = this.h.div({ className: 'resize-south' });
const resizeSE = this.h.div({ className: 'resize-south-east' });
this.buttonBox.appendChild(resizeSouth);
this.buttonBox.appendChild(resizeSE);
new Resizer(resizeSouth, this.resizeElement, el, { v: true });
new Resizer(
resizeSE, this.resizeElement, el,
{ h: true, v: true }, { w: 320 }
);
}
this.buttons.forEach((button, i) => {
const modifiers = [];
if (this.buttons.length >= 2 && !button.greyButton && !i)
modifiers.push(this.buttonClass || 'primary');
const onclick = () => this.buttonHandler(button);
const buttonEl = new Button({
modifiers, onclick, parent: this.buttonBox, text: button.label
}).element();
buttonEl.buttonConfig = button;
});
this.animateButtons(this.buttonBox);
el.appendChild(this.buttonBox);
}
}
Object.assign(Modal.prototype, WCom.Util.Markup);
Object.assign(Modal.prototype, WCom.Util.String);
class ModalUtil {
constructor(args, onSubmit) {
const {
formClass = 'classic',
icons = '/icons.svg',
initValue,
url,
valueStore = {}
} = args;
this.icons = icons;
this.url = url;
this.formClass = formClass;
this.initValue = initValue;
this.valueStore = valueStore;
this.onSubmit = onSubmit;
}
createModalContainer() {
const spinner = this._createSpinner();
const loader = this.h.div({ className: 'modal-loader' }, spinner);
this.frame = this.h.div({
className: 'selector',
id: 'selector-frame',
style: 'visibility:hidden;'
});
const container = this.h.div(
{ className: 'modal-frame-container' }, [loader, this.frame]
);
const options = {
formClass: this.formClass,
onSubmit: this.onSubmit,
renderLocation: function(href) {
this._loadFrameContent(href);
}.bind(this)
};
this.selector = new Selector(this.frame);
this.onload = function() {
loader.style.display = 'none';
const selector = this.selector;
if (this.initValue)
selector.setModalValue(this.initValue, this.valueStore);
for (const anchor of this.frame.querySelectorAll('a')) {
anchor.addEventListener('click', function(event) {
this.valueStore = selector.setValueStore(this.valueStore);
this.initValue = this.valueStore.value;
}.bind(this));
};
WCom.Navigation.manager.scan(this.frame, options);
this.frame.style.visibility = 'visible';
}.bind(this);
this._loadFrameContent(this.url);
return container;
}
getModalValue(success) {
return this.selector.getModalValue(success);
}
_createSpinner(modifierClass = '') {
const icon = this.h.icon({
name: 'spinner', className: 'loading-icon', icons: this.icons
});
return this.h.span({
className: `loading ${modifierClass}`
}, this.h.span({ className: 'loading-spinner' }, icon));
}
async _loadFrameContent(url) {
const opt = { headers: { prefer: 'render=partial' }, response: 'text'};
const { location, text } = await this.bitch.sucks(url, opt);
if (text && text.length > 0) {
this.frame.innerHTML = text;
}
else if (location) {
// TODO: Deal with
}
else {
console.warn('Neither content nor redirect in response to get');
}
if (this.onload) this.onload();
}
}
Object.assign(ModalUtil.prototype, WCom.Util.Bitch);
Object.assign(ModalUtil.prototype, WCom.Util.Markup);
class Resizer {
constructor(el, resizeEl, alsoResize, dir) {
el.addEventListener('mousedown', function(event) {
this.startDrag(event, resizeEl, alsoResize, dir.h, dir.v)
}.bind(this));
this.drag = {};
}
dragHandler(event) {
event.preventDefault();
const { drag } = this;
if (drag.h) {
const width = Math.max(0, drag.width + event.pageX - drag.x);
drag.resizeEl.style.width = width + 'px';
}
if (drag.v) {
const height = Math.max(0, drag.height + event.pageY - drag.y);
drag.resizeEl.style.height = height + 'px';
}
if (drag.alsoResize) {
drag.alsoResize.each(function() {
if (this != drag.resizeEl[0]) {
if (drag.h) this.style.width = width + 'px';
if (drag.v) this.style.height = height + 'px';
}
});
}
}
startDrag(event, resizeEl, alsoResize, h, v) {
event.preventDefault();
const style = {
height: '100px', position: 'absolute',
width: window.getComputedStyle(resizeEl).width
};
const shim = this.h.div({ style: style });
shim.insertBefore(event.target);
const dimensions = this.h.getDimensions(resizeEl);
this.drag = {
alsoResize: alsoResize, h: h, height: dimensions.height,
resizeEl: resizeEl, shim: shim, v: v, width: dimensions.width,
x: event.pageX, y: event.pageY
}
document.addEventListener('mousemove', this.dragHandler.bind(this));
document.addEventListener('mouseup', function(event) {
if (this.drag.shim) {
this.drag.shim.remove();
delete this.drag.shim;
}
document.removeEventListener('mousemove', this.dragHandler);
}.bind(this));
}
}
Object.assign(Resizer.prototype, WCom.Util.Markup);
class Selector {
constructor(frame) {
this.frame = frame;
this.tableClass = 'state-table';
this.displayAttribute = 'object_display';
}
getModalValue(success) {
if (!success) return null;
const els = this._selectionEls();
const selected = [];
const values = [];
for (const el of els) {
if (el.checked) selected.push(el);
values.push(el.value);
}
if (this.type == 'checkbox') {
this.valueStore = {
display: null,
value: this._removeUnchecked(this._addIDs(selected), els)
};
}
else if (this.type == 'radio') {
if (selected && selected.length > 0) {
this.valueStore = {
display: selected[0].getAttribute(this.displayAttribute),
value: selected[0].value
};
}
if (this.valueStore && this.valueStore.value !== undefined) {
const tempHash = {};
tempHash[this.valueStore.value] = 1;
this.valueStore = {
display: this.valueStore.display,
value: this._removeUnchecked(tempHash, els)
};
}
else this.valueStore = null;
}
else if (this.type == 'text') {
this.valueStore = { display: null, value: values };
}
else if (this.type == 'file') {
this.valueStore = { display: null, files: els[0].files };
}
return this.valueStore;
}
setModalValue(value, valueHash) {
if (!value) return;
this.valueStore = valueHash;
const values = value.split(',');
WCom.Modal.clientTablePrecheckedValues = values;
let el;
if (this.type === 'radio') {
for (const selected of this._selectionEls()) {
if (selected.value == value) {
el = selected;
break;
}
}
}
else {
for (const selected of this._selectionEls()) {
if (values.includes(selected.value)) {
el = selected;
break;
}
}
}
if (!el) return;
el.setAttribute('checked', false);
el.click();
}
setValueStore(valueStore) {
this.valueStore = valueStore;
return this.getModalValue(true);
}
_addIDs(selected) {
const values = this.valueStore && this.valueStore.value
? this.valueStore.value.split(',') : [];
const newValues = [];
if (selected && selected.length > 0) {
for (const el of selected) newValues.push(el.value);
}
const valueHash = {};
if (values.length || newValues.length) {
for (const v of [...values, ...newValues]) valueHash[v] = v;
}
return valueHash;
}
_removeUnchecked(values, els) {
if (values.length === 0) return '';
for (const el of els) {
if (values[el.value] && !el.checked) delete values[el.value];
}
return Object.keys(values).join(',');
}
_selectionEls() {
let pattern = 'input[type=';
const table = this.frame.querySelector('.' + this.tableClass);
if (table) pattern = '.' + this.tableClass + ' ' + pattern;
const selector = this.frame.querySelectorAll.bind(this.frame);
if (this.type !== undefined) {
return selector(pattern + this.type + ']');
}
let els = selector(pattern + 'radio]');
if (els && els.length > 0) {
this.type = 'radio';
return els;
}
els = selector(pattern + 'checkbox]');
if (els && els.length > 0) {
this.type = 'checkbox';
return els;
}
els = selector(pattern + 'text]');
if (els && els.length > 0) {
this.type = 'text';
return els;
}
els = selector(pattern + 'file]');
if (els && els.length > 0) {
this.type = 'file';
return els;
}
throw new Error(
'Selectors need either a radio, checkbox, text, or file input'
);
}
}
const create = function(args) {
const {
buttonClass,
callback = () => {},
cancelCallback,
classList = false,
labels = ['Cancel', 'OK'],
noButtons = false,
title,
validateForm
} = args;
let modal;
const onSubmit = function(event) { modal.close() };
const util = new ModalUtil(args, onSubmit);
const container = util.createModalContainer();
let buttons = [];
if (!noButtons) {
buttons = [{
label: labels[0],
onclick(modalObj) {
try { callback(false, modalObj, util.getModalValue(false))}
catch(e) {}
if (cancelCallback) return cancelCallback();
return true;
}
}, {
label: labels[1],
onclick(modalObj) {
const modalValue = util.getModalValue(true);
if (validateForm && !validateForm(modalObj, modalValue))
return false;
return callback(true, modalObj, modalValue);
}
}];
}
const options = {
buttonClass, classList, icons: util.icons, noInner: true
};
if (args.closeCallback) options.closeCallback = args.closeCallback;
modal = new Modal(title, container, buttons, options);
modal.render();
return modal;
};
return {
create,
createAlert: function(args) {
const {
callback = () => {},
classList,
icon = 'info',
label = 'Okay',
text = '',
title
} = args;
const content = document.createElement('div');
content.classList.add(`popup-alert-${icon}`);
content.appendChild(document.createTextNode(text));
const buttons = [{ label, onclick: callback }];
const options = { animate: 'jump', fadeSpeed: 200, classList };
const modal = new Modal(title, content, buttons, options);
modal.render();
return modal;
},
createSelector: function(args) {
const { icons, onchange, target, title = 'Select Item', url } = args;
const callback = function(ok, modal, result) {
if (!ok) return;
const el = document.getElementById(target);
const newValue = result.value.replace(/!/g, '/');
if (onchange && el.value != newValue)
eval(onchange.replace(/%value/g, result.value));
el.value = newValue;
if (el.focus) el.focus();
}.bind(this);
return create({ callback, icons, title, url });
}
};
})();