hazdev-webutils
Version:
Utilities commonly used in web applications developed by the EHP HazDev team.
415 lines (329 loc) • 10.2 kB
JavaScript
;
/**
* Generic class for modal dialog views. Modal dialogs present a blocking
* interface to the user and require user-interaction in order to be closed
* (i.e. clicking a button etc...).
*
* It is important to note that while the interface appears blocked while a
* modal dialog is open, Javascript continues to execute in the background.
*
* Only one modal dialog can be visible at any given time.
*
* If a second modal dialog is opened while the first modal dialog is still
* visible, the first modal dialog is hidden and the second is shown. Upon
* closing the second modal dialog, the first modal dialog is re-shown (unless
* the "clear" method is passed to the hide method). This process continues in a
* last-in, first-out (stack) ordering until all modal dialogs are closed.
*
*/
var Util = require('../util/Util'),
View = require('./View');
var __INITIALIZED__ = false,
_DIALOG_STACK = null,
_FOCUS_STACK = null,
_MASK = null,
_DEFAULTS = {
closable: true, // Should modal box include little "X' in corner
destroyOnHide: false,
title: document.title + ' Says...'
};
var _static_initialize = function () {
// Create the dialog stack
_DIALOG_STACK = [];
// Create the focus stack
_FOCUS_STACK = [];
// Create the modal mask
_MASK = document.createElement('div');
_MASK.classList.add('modal');
__INITIALIZED__ = true;
};
// Note: "this" is a reference to the buttom DOM element and has all the
// proper attributes set on it such that the implementation below is
// correct. It does *not* need to use _this (also it's a static method).
var _buttonCallback = function (evt) {
if (this.info && this.info.callback &&
typeof this.info.callback === 'function') {
this.info.callback(evt, this.modal||{});
}
};
/**
* Pulls the next element off the focus stack and attempts to set the
* focus to it.
*
*/
var _focusNext = function () {
var node;
node = _FOCUS_STACK.pop();
if (node && node instanceof Node && node.focus) {
node.focus();
}
};
var ModalView = function (message, params) {
var _this,
_initialize,
_buttons,
_classes,
_closable,
_closeButton,
_content,
_destroyOnHide,
_footer,
_message,
_title,
_titleEl,
_createButton,
_createViewSkeleton,
_onKeyDown,
_onModalClick;
params = Util.extend({}, _DEFAULTS, params);
_this = View(params);
_initialize = function () {
_buttons = params.buttons;
_classes = params.classes;
_closable = params.closable;
_destroyOnHide = params.destroyOnHide;
_message = message;
_title = params.title;
_this.el.modal = _this;
_createViewSkeleton();
_this.render();
if (!__INITIALIZED__) {
_static_initialize();
}
params = null;
};
_createButton = function (info) {
var i,
len,
button = document.createElement('button'),
buttonInfo;
buttonInfo = Util.extend({}, {
classes: [],
text: 'Click Me',
title: '',
callback: function () {}
}, info);
for (i = 0, len = buttonInfo.classes.length; i < len; i++) {
button.classList.add(buttonInfo.classes[i]);
}
button.innerHTML = buttonInfo.text;
if (buttonInfo.title !== '') {
button.setAttribute('title', buttonInfo.title);
}
button.modal = _this;
button.info = buttonInfo;
if (buttonInfo.callback) {
button.addEventListener('click', _buttonCallback);
}
return button;
};
_createViewSkeleton = function () {
var header, i, len;
Util.empty(_this.el);
_this.el.classList.add('modal-dialog');
// Add custom classes to the view
if (_classes && _classes.length > 0) {
for (i = 0, len = _classes.length; i < len; i++) {
_this.el.classList.add(_classes[i]);
}
}
if (_title) {
header = _this.el.appendChild(document.createElement('header'));
header.classList.add('modal-header');
_titleEl = header.appendChild(document.createElement('h3'));
_titleEl.setAttribute('tabIndex', '-1');
_titleEl.classList.add('modal-title');
if (_closable) {
_closeButton = header.appendChild(document.createElement('span'));
_closeButton.classList.add('modal-close-link');
_closeButton.classList.add('material-icons');
_closeButton.setAttribute('title', 'Close');
_closeButton.innerHTML = 'close';
_closeButton.addEventListener('click', _this.hide);
}
} else {
_this.el.classList.add('no-header');
}
_content = _this.el.appendChild(document.createElement('section'));
_content.setAttribute('tabIndex', '-1');
_content.classList.add('modal-content');
if (_buttons && _buttons.length) {
_footer = _this.el.appendChild(document.createElement('footer'));
_footer.classList.add('modal-footer');
} else {
_this.el.classList.add('no-footer');
}
};
/**
* This method is bound to the ModalView instance using the
* Function.prototype.bind method, thus the reference to "this" is correct
* even though this is a keydown event handler.
*
* @param event {KeyEvent}
* The event that triggered this call.
*/
_onKeyDown = function (event) {
if (event.keyCode === 27) {
_this.hide();
}
};
_onModalClick = function (event) {
if (event.target.className === 'modal') {
_this.hide();
}
};
/**
* Remove event listeners and free references.
*
* You should call hide first.
*/
_this.destroy = function () {
var button;
_MASK.removeEventListener('click', _this.hide);
if (_buttons && _buttons.length) {
while (_footer.childNodes.length > 0) {
button = _footer.firstChild;
button.removeEventListener('click', _buttonCallback);
_footer.removeChild(button);
}
}
if (_closeButton) {
_closeButton.removeEventListener('click', _this.hide);
_closeButton = null;
}
delete _this.el.modal;
_footer = null;
_titleEl = null;
_content = null;
_destroyOnHide = null;
_this.el = null;
_onModalClick = null;
};
_this.hide = function (clearAll) {
var isVisible;
isVisible = (_this.el.parentNode === _MASK);
if (clearAll === true) {
// Remove any/all dialogs attached to _MASK
Util.empty(_MASK);
// Clear stack of previous dialogs to return user to normal application.
_DIALOG_STACK.splice(0, _DIALOG_STACK.length);
// Clear all but last focus element
_FOCUS_STACK.splice(1, _FOCUS_STACK.length);
_focusNext();
if (isVisible) { // Or rather, was visible
_this.trigger('hide', _this);
if (_destroyOnHide) {
_this.destroy();
}
}
} else if (isVisible) {
// This modal is currently visible
_this.el.parentNode.removeChild(_this.el);
// Check if any other dialogs exist in stack, if so, show it
if (_DIALOG_STACK.length > 0) {
_DIALOG_STACK.pop().show();
}
_focusNext();
_this.trigger('hide', _this);
if (_destroyOnHide) {
_this.destroy();
}
}
if (!_MASK.firstChild && _MASK.parentNode) {
// No more dialogs, remove the _MASK
_MASK.parentNode.removeChild(_MASK);
_MASK.removeEventListener('click', _onModalClick);
document.body.classList.remove('backgroundScrollDisable');
window.removeEventListener('keydown', _onKeyDown);
}
return _this;
};
_this.render = function (message) {
var m = message || _message,
button = null,
buttons = _buttons || [],
i, len = buttons.length;
// Set the modal dialog content
Util.empty(_content);
if (typeof m === 'string') {
_content.innerHTML = m;
} else if (typeof m === 'function') {
return _this.render(m(_this));
} else if (m instanceof Node) {
_content.appendChild(m);
}
// Set the modal dialog title
if (_title) {
_titleEl.innerHTML = _title;
}
// Clear any old footer content
if (_buttons && _buttons.length) {
while (_footer.childNodes.length > 0) {
button = _footer.firstChild;
Util.removeEvent(button, 'click', _buttonCallback);
_footer.removeChild(button);
}
}
// Set new footer content
for (i = 0; i < len; i++) {
_footer.appendChild(_createButton(buttons[i]));
}
_this.trigger('render', _this);
return _this;
};
_this.setMessage = function (message) {
_message = message;
_this.trigger('message', _this);
return _this;
};
_this.setOptions = function (params, extend) {
if (extend) {
params = Util.extend({}, {
buttons: _buttons,
classes: _classes,
closable: _closable,
message: _message,
title: _title
}, params);
}
_buttons = params.buttons;
_classes = params.classes;
_closable = params.closable;
_message = message;
_title = params.title;
_this.trigger('options', _this);
return _this;
};
_this.show = function () {
var oldChild = null;
// For accessibility, focus the top of this new dialog
_FOCUS_STACK.push(document.activeElement || false);
// Mask already has a dialog in it, add to dialog stack and continue
while (_MASK.firstChild) {
oldChild = _MASK.firstChild;
if (oldChild.modal) {
_DIALOG_STACK.push(oldChild.modal);
}
_MASK.removeChild(oldChild);
}
// Add this dialog to the mask
_MASK.appendChild(_this.el);
_MASK.addEventListener('click', _onModalClick);
// Show the mask if not yet visible
if (!_MASK.parentNode) {
document.body.appendChild(_MASK);
document.body.classList.add('backgroundScrollDisable');
window.addEventListener('keydown', _onKeyDown);
}
if (_title) {
_titleEl.focus();
} else {
_content.focus();
}
_this.trigger('show', _this);
return _this;
};
_initialize();
return _this;
};
module.exports = ModalView;