2019-09-15 18:33:45 +02:00
|
|
|
import morphdom from 'morphdom';
|
|
|
|
import EventEmitter from 'events';
|
|
|
|
import { Tip } from 'util/ui/tip';
|
|
|
|
import { KeyHandler } from 'comp/browser/key-handler';
|
|
|
|
import { Logger } from 'util/logger';
|
|
|
|
|
2019-09-16 21:49:21 +02:00
|
|
|
const OnlyDirectEvents = {
|
|
|
|
mouseenter: true,
|
|
|
|
mouseleave: true
|
|
|
|
};
|
|
|
|
|
2019-09-15 18:33:45 +02:00
|
|
|
class View extends EventEmitter {
|
|
|
|
parent = undefined;
|
|
|
|
template = undefined;
|
|
|
|
events = {};
|
2019-09-16 17:43:57 +02:00
|
|
|
model = undefined;
|
|
|
|
options = {};
|
2019-09-15 18:33:45 +02:00
|
|
|
views = {};
|
|
|
|
hidden = false;
|
|
|
|
removed = false;
|
2019-09-16 21:49:21 +02:00
|
|
|
eventListeners = {};
|
2019-09-15 18:33:45 +02:00
|
|
|
debugLogger = localStorage.debugViews ? new Logger('view', this.constructor.name) : undefined;
|
|
|
|
|
2019-09-16 19:24:15 +02:00
|
|
|
constructor(model = undefined, options = {}) {
|
2019-09-15 18:33:45 +02:00
|
|
|
super();
|
|
|
|
|
|
|
|
this.model = model;
|
2019-09-16 17:43:57 +02:00
|
|
|
this.options = options;
|
2019-09-15 23:02:51 +02:00
|
|
|
|
|
|
|
this.setMaxListeners(100);
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
render(templateData) {
|
|
|
|
if (this.removed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-16 17:58:44 +02:00
|
|
|
let ts;
|
|
|
|
if (this.debugLogger) {
|
|
|
|
this.debugLogger.debug('Render start');
|
|
|
|
ts = this.debugLogger.ts();
|
|
|
|
}
|
2019-09-15 18:33:45 +02:00
|
|
|
|
|
|
|
if (this.el) {
|
|
|
|
Tip.destroyTips(this.el);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.renderElement(templateData);
|
|
|
|
|
|
|
|
Tip.createTips(this.el);
|
|
|
|
|
2019-09-16 17:58:44 +02:00
|
|
|
this.debugLogger && this.debugLogger.debug('Render finished', this.debugLogger.ts(ts));
|
2019-09-15 18:33:45 +02:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderElement(templateData) {
|
|
|
|
const html = this.template(templateData);
|
|
|
|
if (this.el) {
|
2019-09-16 17:43:57 +02:00
|
|
|
const mountRoot = this.options.ownParent ? this.el.firstChild : this.el;
|
|
|
|
morphdom(mountRoot, html);
|
2019-09-15 18:33:45 +02:00
|
|
|
} else {
|
2019-09-16 17:43:57 +02:00
|
|
|
let parent = this.options.parent || this.parent;
|
|
|
|
if (parent) {
|
2019-09-15 20:09:28 +02:00
|
|
|
if (typeof parent === 'string') {
|
2019-09-16 17:43:57 +02:00
|
|
|
parent = document.querySelector(parent);
|
2019-09-15 20:09:28 +02:00
|
|
|
}
|
2019-09-15 18:33:45 +02:00
|
|
|
if (!parent) {
|
2019-09-15 20:09:28 +02:00
|
|
|
throw new Error(`Error rendering ${this.constructor.name}: parent not found`);
|
|
|
|
}
|
2019-09-16 17:43:57 +02:00
|
|
|
if (this.options.replace) {
|
2019-09-15 20:09:28 +02:00
|
|
|
Tip.destroyTips(parent);
|
|
|
|
parent.innerHTML = '';
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
2019-09-15 20:09:28 +02:00
|
|
|
const el = document.createElement('div');
|
|
|
|
el.innerHTML = html;
|
2019-09-16 17:43:57 +02:00
|
|
|
const root = el.firstChild;
|
|
|
|
if (this.options.ownParent) {
|
2019-09-16 17:58:44 +02:00
|
|
|
if (root) {
|
|
|
|
parent.appendChild(root);
|
|
|
|
}
|
2019-09-16 17:43:57 +02:00
|
|
|
this.el = parent;
|
|
|
|
} else {
|
|
|
|
this.el = root;
|
|
|
|
parent.appendChild(this.el);
|
|
|
|
}
|
2019-09-16 21:49:21 +02:00
|
|
|
this.bindEvents();
|
2019-09-15 18:33:45 +02:00
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`Error rendering ${this.constructor.name}: I don't know how to insert the view`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
this.$el = $(this.el); // legacy
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bindEvents() {
|
2019-09-16 21:49:21 +02:00
|
|
|
const eventsMap = {};
|
2019-09-15 18:33:45 +02:00
|
|
|
for (const [eventDef, method] of Object.entries(this.events)) {
|
|
|
|
const spaceIx = eventDef.indexOf(' ');
|
2019-09-16 21:49:21 +02:00
|
|
|
let event, selector;
|
2019-09-15 18:33:45 +02:00
|
|
|
if (spaceIx > 0) {
|
|
|
|
event = eventDef.substr(0, spaceIx);
|
2019-09-16 21:49:21 +02:00
|
|
|
selector = eventDef.substr(spaceIx + 1);
|
|
|
|
if (OnlyDirectEvents[event]) {
|
|
|
|
throw new Error(
|
|
|
|
`Event listener ${eventDef} defined in ${this.constructor.name} ` +
|
|
|
|
`can be installed only on the view itself`
|
|
|
|
);
|
|
|
|
}
|
2019-09-15 18:33:45 +02:00
|
|
|
} else {
|
|
|
|
event = eventDef;
|
|
|
|
}
|
2019-09-16 21:49:21 +02:00
|
|
|
if (!eventsMap[event]) {
|
|
|
|
eventsMap[event] = [];
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
2019-09-16 21:49:21 +02:00
|
|
|
eventsMap[event].push({ selector, method });
|
|
|
|
}
|
|
|
|
for (const [event, handlers] of Object.entries(eventsMap)) {
|
|
|
|
this.debugLogger && this.debugLogger.debug('Bind', event, handlers);
|
|
|
|
const listener = e => this.eventListener(e, handlers);
|
|
|
|
this.eventListeners[event] = listener;
|
|
|
|
this.el.addEventListener(event, listener);
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unbindEvents() {
|
2019-09-16 21:49:21 +02:00
|
|
|
for (const [event, listener] of Object.values(this.eventListeners)) {
|
|
|
|
this.el.removeEventListener(event, listener);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
eventListener(e, handlers) {
|
|
|
|
this.debugLogger && this.debugLogger.debug('Listener fired', e.type);
|
|
|
|
for (const { selector, method } of handlers) {
|
|
|
|
if (selector) {
|
|
|
|
const closest = e.target.closest(selector);
|
|
|
|
if (!closest || !this.el.contains(closest)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!this[method]) {
|
|
|
|
this.debugLogger && this.debugLogger.debug('Method not defined', method);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this.debugLogger && this.debugLogger.debug('Handling event', e.type, method);
|
|
|
|
this[method](e);
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
remove() {
|
|
|
|
this.emit('remove');
|
|
|
|
|
|
|
|
this.removeInnerViews();
|
|
|
|
Tip.hideTips(this.el);
|
|
|
|
this.el.remove();
|
|
|
|
this.removed = true;
|
|
|
|
|
|
|
|
this.debugLogger && this.debugLogger.debug('Remove');
|
|
|
|
}
|
|
|
|
|
|
|
|
removeInnerViews() {
|
|
|
|
if (this.views) {
|
|
|
|
for (const view of Object.values(this.views)) {
|
|
|
|
if (view) {
|
|
|
|
if (view instanceof Array) {
|
|
|
|
view.forEach(v => v.remove());
|
|
|
|
} else {
|
|
|
|
view.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.views = {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
listenTo(model, event, callback) {
|
|
|
|
const boundCallback = callback.bind(this);
|
|
|
|
model.on(event, boundCallback);
|
|
|
|
this.once('remove', () => model.off(event, boundCallback));
|
|
|
|
}
|
|
|
|
|
|
|
|
hide() {
|
|
|
|
Tip.hideTips(this.el);
|
|
|
|
return this.toggle(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
return this.toggle(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
toggle(visible) {
|
2019-09-16 17:58:44 +02:00
|
|
|
this.debugLogger && this.debugLogger.debug(visible ? 'Show' : 'Hide');
|
2019-09-15 18:33:45 +02:00
|
|
|
if (visible === undefined) {
|
|
|
|
visible = this.hidden;
|
|
|
|
}
|
|
|
|
this.hidden = !visible;
|
|
|
|
this.emit(visible ? 'show' : 'hide');
|
2019-09-15 21:22:23 +02:00
|
|
|
if (this.el) {
|
|
|
|
this.el.classList.toggle('show', !!visible);
|
|
|
|
this.el.classList.toggle('hide', !visible);
|
|
|
|
if (!visible) {
|
|
|
|
Tip.hideTips(this.el);
|
|
|
|
}
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isHidden() {
|
|
|
|
return this.hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
isVisible() {
|
|
|
|
return !this.hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
afterPaint(callback) {
|
|
|
|
requestAnimationFrame(() => requestAnimationFrame(callback));
|
|
|
|
}
|
|
|
|
|
|
|
|
onKey(key, handler, shortcut, modal, noPrevent) {
|
|
|
|
KeyHandler.onKey(key, handler, this, shortcut, modal, noPrevent);
|
|
|
|
this.once('remove', () => KeyHandler.offKey(key, handler, this));
|
|
|
|
}
|
2019-09-15 23:02:51 +02:00
|
|
|
|
|
|
|
off(event, listener) {
|
|
|
|
if (listener === undefined) {
|
|
|
|
return super.removeAllListeners(event);
|
|
|
|
} else {
|
|
|
|
return super.off(event, listener);
|
|
|
|
}
|
|
|
|
}
|
2019-09-15 18:33:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export { View };
|