/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/* eslint-env browser */
define("dialogs", [
"jquery",
"Promise",
"underscore",
'nmodule/js/rc/asyncUtils/asyncUtils',
"lex!js",
"hbs!nmodule/js/rc/dialogs/loader",
"css!nmodule/js/rc/dialogs/dialogs" ], function (
$,
Promise,
_,
asyncUtils,
lexicons,
loaderTemplate) {
"use strict";
const [ jsLex ] = lexicons;
const { $deferred } = asyncUtils;
const { escape, findWhere, noop, pluck, throttle } = _;
const notHtmlEscaped = (content) => content;
const tplLoadingTitle = (title) => `<div class='js-dialog-loading'>${ escape(title) }</div>`;
const tplLoadingContent = (content) => {
let loaderTxt;
let loaderImg = content;
if (content && typeof content !== "string") {
loaderImg = content.loaderImg;
loaderTxt = content.loaderTxt;
}
return `
${ loaderTxt ? tplLoadingMsg(loaderTxt) : '' }
<img class='js-dialog-loading-img' src='${ escape(loaderImg) }'/>
`;
};
const tplLoadingMsg = (loaderTxt) => `
<div class='js-dialog-loading-txt'>${ escape(loaderTxt) }</div>
`;
const tplHeader = ({ title, loading }) => `
<div class='js-dialog-header ux-bg'>${ loading ? tplLoadingTitle(title) : escape(title) }</div>
`;
const tplContent = ({ content, isTextContent, loading }) => {
let classList = `js-dialog-content-wrapper js-dialog-content ux-fullscreen-support`;
if (isTextContent) {
classList += ' js-dialog-wrapTextContent';
}
return `
<div class="${ classList }">${
loading ? tplLoadingContent(content) : notHtmlEscaped(content)
}</div>
`;
};
const tplButton = ({ displayName, name }) => `
<li>
<button type='button' class='ux-btn js-dialog-button js-dialog-button-${ escape(name) }'>${ escape(displayName || name) }</button>
</li>
`;
const tplButtons = (buttons) => `
<ul class='js-dialog-button-content ux-fg'>${ buttons.map(tplButton).join('') }</ul>
`;
const tplDialog = ({ title, loading, content, isTextContent, buttons }) => `
<div class='js-dialog-container bajaux-widget-container' style="display:none;">
<div class='js-dialog ux-root ux-fg ux-shadow ux-border'>
${ title ? tplHeader({ title, loading }) : '' }
${ content ? tplContent({ content, isTextContent, loading }) : '' }
${ buttons && buttons.length ? tplButtons(buttons) : '' }
</div>
</div>
`;
const defaultLoadImg = loaderTemplate();
const unknownErr = 'Unknown error';
let openDialogs = [];
let resizeHandler;
/**
* This is a convenience method used for working with functions that take
* an Object Literal as an argument.
*
* This method always ensures an Object is returned so its properties can be
* further validated.
*
* If the obj param is an object literal, this will return a copy of it to ensure the original
* object is not mutated causing unintended issues from dialogs.
*
* @inner
*/
function objectify(obj, propName) {
if (!(obj === undefined || obj === null)) {
if (obj.constructor === Object) {
return Object.assign({}, obj);
} else if (typeof propName === "string") {
if (typeof obj === 'function') {
propName = "content";
}
var o = {};
o[propName] = obj;
return o;
}
}
return {};
}
/**
* A default button handler that returns a resolved promise.
*
* @inner
*
* @returns {Promise}
*/
function defaultButtonHandler() {
return Promise.resolve();
}
/**
* Define a default value for possibly undefined variables.
*
* @inner
*
* @param val The value to be tested.
* @param defVal The default value to be returned if the value is undefined.
* @returns The default value if the value is undefined.
*/
function def(val, defVal) {
return val === undefined ? defVal : val;
}
////////////////////////////////////////////////////////////////
// Button DOM
////////////////////////////////////////////////////////////////
/**
* Return true if the Button's DOM Element is in a state
* where it can be clicked (i.e. it's not hidden or disabled).
*
* @inner
*
* @param {JQuery} buttonJq
* @return {Boolean} true if the button can be clicked.
*/
function canClick(buttonJq) {
return buttonJq &&
!$("button", buttonJq).attr("disabled") &&
buttonJq.css("display") !== "none";
}
/**
* Set the button to be enabled/disabled.
*
* @inner
*
* @param {JQuery} buttonJq The DOM for the button.
* @param {Boolean} enable
*/
function setButtonEnable(buttonJq, enable) {
var b = $("button", buttonJq);
if (enable) {
b.removeAttr("disabled");
} else {
b.attr("disabled", "disabled");
}
}
/**
* Show or hide a button.
*
* @inner
*
* @param {JQuery} buttonJq The button's DOM.
* @param {boolean} show
*/
function setButtonShow(buttonJq, show) {
if (show) {
buttonJq.show();
} else {
buttonJq.hide();
}
}
/**
* Set the data on the button's DOM.
*
* @inner
*
* @param {JQuery} buttonJq The button's DOM.
* @param {Object} data
*/
function setButtonData(buttonJq, data) {
$("button", buttonJq).data("js-dialog-button", data);
}
/**
* Return the button object for the button name if it exists.
*
* @param {module:dialogs~Dialog} dlg The Dialog instance.
* @param {String} name The name of the button.
* @return {module:dialogs~Button|undefined} the button data.
*/
function findButton(dlg, name) {
return findWhere(dlg.$params.buttons, { name });
}
/**
* Return the button object for the button name.
*
* @param {module:dialogs~Dialog} dlg The Dialog instance.
* @param {String} name The name of the button.
* @return {module:dialogs~Button} the button data.
* @throws {Error} if button was not found.
*/
function requireButton(dlg, name) {
const button = findButton(dlg, name);
if (!button) {
throw new Error("Unable to find dialog button: " + name);
}
return button;
}
/**
* Find the button DOM under the dialog's DOM via its name.
*
* @param {JQuery} dialogJq The Dialog's DOM object.
* @param {String} name The name of the button to look up.
* @return {JQuery} The button's DOM object.
*/
function findButtonDom(dialogJq, name) {
return $(".js-dialog-button-" + name, dialogJq).parent();
}
////////////////////////////////////////////////////////////////
// DOM Creation
////////////////////////////////////////////////////////////////
/**
* Initialize the DOM for the button.
*
* @param {module:dialogs~Dialog} dlg
* @param {JQuery} buttonJq
* @param {Object} buttonData
*/
function initButtonDom(dlg, buttonJq, buttonData) {
buttonData.handlerArray = [ buttonData.handler || defaultButtonHandler ];
var name = buttonData.name,
displayName = buttonData.displayName || name,
handlerArray = buttonData.handlerArray,
hide = buttonData.hide,
disable = buttonData.disable;
buttonData.jq = buttonJq;
// Ensure the button name is always defined.
if (typeof name !== "string") {
throw new Error("Dialog button has no name!");
}
buttonJq.on('click', function () {
var args = [ dlg ].concat(Array.prototype.slice.call(arguments)),
i,
promises = [],
retVal;
if (!canClick(buttonJq)) {
return;
}
try {
for (i = 0; i < handlerArray.length; ++i) {
try {
retVal = handlerArray[i].apply(this, args);
} catch (e) {
return dlg.close(name, /*fail*/ e || new Error());
}
// If a handler returns false then this means we need to use a
// rejected promise.
if (retVal === false) {
retVal = Promise.reject(new Error());
}
// When the button is clicked invoke the handler and hold any promises returned
promises.push(retVal);
}
// Close the dialog once all the promises have been resolved
Promise.all(promises)
.then(function (results) {
let result;
results.forEach((r) => { if (r !== undefined) { result = r; } });
dlg.$close({ name, result });
})
.catch(noop);
} finally {
$(window).trigger("jsdialog:buttonpressed",
[ dlg, name, displayName, handlerArray ]);
}
});
if (disable) {
setButtonEnable(buttonJq, /*enable*/false);
}
if (hide) {
setButtonShow(buttonJq, /*show*/false);
}
setButtonData(buttonJq, buttonData);
}
/**
* Initialize the DOM for the Dialog box.
*
* @inner
*
* @param {module:dialogs~Dialog} dlg
*/
function initDom(dlg) {
const { buttons, title, content, loading, fade, cancel, no, yes, ok, text } = dlg.$params;
const templateData = {
title,
content: typeof content === "function" ? "<span></span>" : content,
isTextContent: text && text.length,
loading,
buttons,
fade
};
if (cancel) {
buttons.push({
name: "cancel",
handler: cancel,
esc: true
});
}
if (no) {
buttons.unshift({
name: "no",
handler: no
});
}
if (yes) {
buttons.unshift({
name: "yes",
handler: yes
});
}
if (ok) {
buttons.unshift({
name: "ok",
handler: ok
});
}
// Initialize each button displayName
buttons.forEach((bt) => {
bt.displayName = getButtonDisplayName(bt.name, bt.displayName);
});
const dialogJq = $(tplDialog(templateData));
findMainDialogElement(dialogJq).data("js-dialog", dlg);
// Initialize the buttons
buttons.forEach((bt) => {
initButtonDom(dlg, findButtonDom(dialogJq, bt.name), bt);
});
dlg.$dialogJq = dialogJq;
}
/**
* Get the displayName for a given button.
* @param {String} name
* @param {String} [displayName]
* @returns {String}
*/
function getButtonDisplayName(name, displayName) {
if (displayName) {
return displayName;
}
switch (name) {
case 'ok':
case 'cancel':
case 'yes':
case 'no':
return jsLex.get("dialogs." + name) || name;
}
return name;
}
function findMainDialogElement(dialogJq) {
return dialogJq.children('.js-dialog');
}
function findContentElement(dialogJq) {
return findMainDialogElement(dialogJq).children('.js-dialog-content');
}
function findButtonsElement(dialogJq) {
return findMainDialogElement(dialogJq).children('.js-dialog-button-content');
}
function findHeaderElement(dialogJq) {
return findMainDialogElement(dialogJq).children('.js-dialog-header');
}
////////////////////////////////////////////////////////////////
// Dialog
////////////////////////////////////////////////////////////////
/**
* A class for a Dialog box.
*
* An instance of a Dialog can be accessed indirectly by use of
* one of the showXxx methods.
*
* @class
* @inner
* @public
* @memberOf module:dialogs
*
* @example
* <caption>Show a basic simple OK Dialog box</caption>
* dialogs.showOk("Here's a nice OK dialog box!");
*/
var Dialog = function Dialog(params) {
var that = this;
params = objectify(params, "text");
//do not allow both content and text parameters
if (params.content && params.text) {
throw new Error('The "text" and "content" parameters are not allowed at the same time');
}
// Set up default dialog parameters
params.title = def(params.title, "");
params.buttonNames = params.buttonNames || {};
params.buttons = params.buttons || [];
params.parent = params.parent || $("body");
params.private = params.private || false;
// if text is supplied then set the content to that instead of the content parameter
if (params.text) {
params.content = escape(def(params.text, ""));
} else {
params.content = def(params.content, "");
}
that.$params = params;
that.$closed = false;
that.$hidden = false;
that.$dialogJq = null;
// Inner deferred promise. This promise will be resolved
// when the dialog is closed.
that.$df = $deferred();
// Create the DOM but don't attach it.
initDom(that);
};
function cancelDelayId(dlg) {
if (dlg.$delayId) {
clearTimeout(dlg.$delayId);
}
}
/**
* When a dialog is shown or the window is resized, the content wrapper
* element needs to have its max-height set to prevent the dialog buttons
* from being scrolled off the bottom of the page.
*
* @param {module:dialogs~Dialog} dlg
*/
function layout(dlg) {
if (!resizeHandler) {
resizeHandler = throttle(function () {
dialogs.each(function (i, dlg) { layout(dlg); });
}, 1000 / 60);
$(window).on('resize', resizeHandler);
}
var layoutParam = dlg.$params.layout;
if (typeof layoutParam === 'function') {
layoutParam(dlg);
}
}
/**
* Show the Dialog.
*
* @returns {module:dialogs~Dialog}
*
* @example
* <caption>Create a blank dialog box and then show it.</caption>
* dialogs.make("A dialog box with no buttons!")
* .show();
*/
Dialog.prototype.show = function show() {
var that = this,
dialogJq = that.$dialogJq;
cancelDelayId(that);
function autoFocus() {
//Only call focus on the first button if the top most dialog is mine and there is a button.
const topDialogFirstButtonJq = $(".js-dialog-button-content:last button:first");
const topDialogFirstButtonElem = topDialogFirstButtonJq[0];
const dialogElem = dialogJq && dialogJq[0];
if (topDialogFirstButtonElem && dialogElem && dialogElem.contains(topDialogFirstButtonElem)) {
topDialogFirstButtonJq.focus();
}
}
//Sets the background opacity to screen the contents when the dialog is showing
function setPrivate() {
if (that.$params.private) {
dialogJq.addClass("js-dialog-container-private");
}
}
if (!that.$closed) {
// Lazily attach the dialog to the DOM
if (dialogJq.parent().length === 0) {
openDialogs.push(that);
that.$params.parent.append(dialogJq);
$(window).trigger("jsdialog:created", [ that ]);
}
that.$hidden = false;
if (that.$params.fade) {
dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
dialogJq.fadeIn("fast", autoFocus);
} else {
dialogJq.show();
setPrivate();
autoFocus();
}
layout(that);
}
$(window).trigger("jsdialog:show", [ that ]);
return that;
};
/**
* Hide the Dialog without closing it. The preferred method
* to use is close.
*
* @returns {module:dialogs~Dialog}
*
* @example
* <caption>
* Hide a dialog box after 2 seconds
* </caption>
* dialogs.showYesNo("Meeting alert! Do you want to be reminded in 10 seconds?")
* .yes(function (dialog) {
* dialog.hide();
* setTimeout(function () {
* dialog.show();
* }, 10000);
* return false;
* });
*/
Dialog.prototype.hide = function hide() {
var that = this,
dialogJq = that.$dialogJq;
cancelDelayId(that);
if (!that.$closed) {
that.$hidden = true;
if (that.$params.fade) {
dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
dialogJq.fadeOut("fast");
} else {
dialogJq.hide();
}
$(window).trigger("jsdialog:hide", [ that ]);
}
return that;
};
function removeFromOpenDialogs(dlg) {
// Remove the dialog from the array
var i;
for (i = 0; i < openDialogs.length; ++i) {
if (openDialogs[i] === dlg) {
openDialogs.splice(i, 1);
break;
}
}
}
/**
* Close the Dialog. This will remove the Dialog box from the screen.
*
* @param {String} [name] The name of the button used to close the dialog box.
* This parameter is designed to be called from the Dialog JS framework itself.
* @param {*} [fail] optional failure reason. If truthy, the dialog's promise
* will be rejected with this failure reason; otherwise, the promise will be
* resolved.
* @returns {module:dialogs~Dialog}
*
* @example
* <caption>
* Open a Dialog and close it after 2 seconds
* </caption>
* var dlg = dialogs.showOk("A notification");
*
* setTimeout(function () {
* dlg.close();
* }, 2000);
*/
Dialog.prototype.close = function close(name, fail) {
return this.$close({ name, fail });
};
/**
* @private
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.$close = function ({ name, fail, result }) {
const that = this;
const dialogJq = that.$dialogJq;
const df = that.$df;
name = name || "cancel";
that.$closed = true;
cancelDelayId(that);
if (dialogJq) {
dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
if (that.$params.fade) {
dialogJq.fadeOut("fast", function () {
dialogJq.remove();
});
} else {
dialogJq.remove();
}
removeFromOpenDialogs(that);
$(window).trigger("jsdialog:close", [ that ]);
}
// Once closed, resolve the promise with the specified name
if (!fail) {
df.resolve([ that, name, result ]);
} else {
df.reject([ that, name, fail ]);
}
return that;
};
/**
* Move the Dialog to the front.
*
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.toFront = function toFront() {
var that = this,
dialogJq = that.$dialogJq;
if (dialogJq && !that.$closed && openDialogs.length > 1) {
findMainDialogElement(dialogJq).stop(/*clearQueue*/true, /*jumpToEnd*/true);
removeFromOpenDialogs(that);
openDialogs.push(that);
that.$params.parent.append(dialogJq);
}
return that;
};
/**
* Move the Dialog to the back.
*
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.toBack = function toBack() {
var that = this,
dialogJq = that.$dialogJq;
if (dialogJq &&
!that.$closed &&
openDialogs.length > 1 &&
openDialogs[1].$dialogJq) {
findMainDialogElement(dialogJq).stop(/*clearQueue*/true, /*jumpToEnd*/true);
removeFromOpenDialogs(that);
openDialogs.unshift(that);
dialogJq.insertBefore(openDialogs[1].$dialogJq);
}
return that;
};
/**
* Return true if the Dialog is closed and removed from the DOM.
*
* @returns {Boolean} Return true if the Dialog has been closed.
*/
Dialog.prototype.isClosed = function isClosed() {
return this.$closed;
};
/**
* Return true if the Dialog is hidden.
*
* @returns {Dialog} Return true if the Dialog has been hidden.
*/
Dialog.prototype.isHidden = function isHidden() {
return this.$hidden;
};
/**
* Add a callback handler for a button via its name. This callback
* handler will be invoked when the button is clicked.
*
* Any handler function can return a Promise. This
* can control when and if the Dialog box closes after the handler
* has been invoked. It should be noted that multiple handlers
* can be registered on a button.
*
* * If the handlers return nothing, the Dialog will be closed after
* all the Handlers have been invoked.
* * If one or more handlers return a Promise, the Dialog
* will only close after all the Promises have been resolved.
* * If one of the Promises is rejected, the Dialog will not close.
*
* @param {String} name The name of the button to register the handler on.
* @param {function(module:dialogs~Dialog): Promise} handler The handler of the function to be
* invoked when the button is clicked. When invoked, the first argument of the
* handler is the Dialog instance.
* @returns {module:dialogs~Dialog}
*
* @see module:dialogs~Dialog#ok
* @see module:dialogs~Dialog#cancel
* @see module:dialogs~Dialog#yes
* @see module:dialogs~Dialog#no
*
* @example
* <caption>
* Register a function be to be called when the 'foo' button
* is clicked.
* </caption>
* dialogs.show({
* content: "Show some stuff",
* buttons: [
* {
* name: "foo",
* handler: function () {
* alert("First annoying alert!");
* }
* }
* ]
* }).on("foo", function () {
* alert("This will also be called when foo button is clicked.");
* });
*/
Dialog.prototype.on = function on(name, handler) {
requireButton(this, name).handlerArray.push(handler);
return this;
};
/**
* Click one of the Dialog's buttons.
*
* @example
* <caption>
* Show a Dialog with an OK button and click it 2 seconds later
* </caption>
* var dlg = dialogs.showOk("This is an OK Dialog")
* setTimeout(function () {
* dlg.click("ok")
* }, 2000);
*
* @param {String} name The name of the Dialog button to click.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.click = function click(name) {
var bt = requireButton(this, name);
if (canClick(bt.jq)) {
bt.jq.trigger('click');
}
return this;
};
function onButton(dlg, name, handler) {
if (handler) {
return dlg.on(name, handler);
} else {
return dlg.click(name);
}
}
/**
* Add a 'ok' handler to the Dialog or if no handler is specified,
* simulate clicking the Dialog's 'ok' button.
*
* @see module:dialogs~Dialog#on
*
* @param {Function} [handler] if specified, the handler to be invoked
* when the Dialog's 'ok' button is clicked. The first argument of the
* function callback is the Dialog instance.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.ok = function ok(handler) {
return onButton(this, "ok", handler);
};
/**
* Add a 'cancel' handler to the Dialog or if no handler is specified,
* simulate clicking the Dialog's 'cancel' button.
*
* @see module:dialogs~Dialog#on
*
* @param {Function} [handler] if specified, the handler to be invoked
* when the Dialog's 'cancel' button is clicked. The first argument of the
* function callback is the Dialog instance.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.cancel = function cancel(handler) {
return onButton(this, "cancel", handler);
};
/**
* Add a 'yes' handler to the Dialog or if no handler is specified,
* simulate clicking the Dialog's 'yes' button.
*
* @see module:dialogs~Dialog#on
*
* @param {Function} [handler] if specified, the handler to be invoked
* when the Dialog's 'yes' button is clicked. The first argument of the
* function callback is the Dialog instance.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.yes = function yes(handler) {
return onButton(this, "yes", handler);
};
/**
* Add a 'no' handler to the Dialog or if no handler is specified,
* simulate clicking the Dialog's 'no' button. The first argument of the
* function callback is the Dialog instance.
*
* @see module:dialogs~Dialog#on
*
* @param {Function} [handler] if specified, the handler to be invoked
* when the Dialog's 'no' button is clicked.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.no = function no(handler) {
return onButton(this, "no", handler);
};
/**
* Return the button DOM for the given name.
*
* @param {String} name The name of the button.
* @returns {JQuery|null} the Button's jQuery DOM object or null if nothing found.
*/
Dialog.prototype.buttonJq = function buttonJq(name) {
const button = findButton(this, name);
return button ? button.jq : null;
};
/**
* Disable a button.
*
* @param {String} name The name of the button.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.disableButton = function disableButton(name) {
setButtonEnable(this.buttonJq(name), /*enable*/false);
return this;
};
/**
* Enable a button.
*
* @param {String} name The name of the button.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.enableButton = function enableButton(name) {
setButtonEnable(this.buttonJq(name), /*enable*/true);
return this;
};
/**
* Show a button.
*
* @param {String} name The name of the button.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.showButton = function showButton(name) {
setButtonShow(this.buttonJq(name), /*show*/true);
return this;
};
/**
* Hide a button.
*
* @param {String} name The name of the button.
* @returns {module:dialogs~Dialog}
*/
Dialog.prototype.hideButton = function hideButton(name) {
setButtonShow(this.buttonJq(name), /*show*/false);
return this;
};
/**
* Adds a new button to this dialog. If a button with the same name already exists, no
* action will be taken.
*
* @private
* @param {module:dialogs~Button} btn
* @since Niagara 4.12
*/
Dialog.prototype.$addButton = function (btn) {
const { name } = btn;
if (findButton(this, name)) { return; }
const dialogJq = this.$dialogJq;
const buttonsElement = findButtonsElement(dialogJq);
const buttons = this.$params.buttons;
let btnJq;
if (buttonsElement.length) {
btnJq = $(tplButton(btn)).appendTo(buttonsElement);
} else {
findMainDialogElement(dialogJq).append(tplButtons([ btn ]));
btnJq = findButtonDom(dialogJq, name);
}
buttons.push(btn);
initButtonDom(this, btnJq, btn);
// ensure 'cancel' is always last.
const buttonNames = pluck(buttons, 'name');
const cancelIndex = buttonNames.indexOf('cancel');
if (cancelIndex >= 0) {
buttonNames.push(buttonNames.splice(cancelIndex, 1)[0]);
this.$reorderButtons(buttonNames);
}
layout(this);
};
/**
* Reorders the buttons in this dialog. This can be done in two ways:
*
* - A sort function that receives and compares two instances of {@link module:dialogs~Button}.
* - An array of strings indicating the desired ordering of button names.
*
* @private
* @param {Function|Array.<string>} sort
* @since Niagara 4.12
*/
Dialog.prototype.$reorderButtons = function (sort) {
if (Array.isArray(sort)) {
const arr = sort;
sort = (a, b) => {
let indexA = arr.indexOf(a.name);
let indexB = arr.indexOf(b.name);
if (indexA < 0) { indexA = Number.POSITIVE_INFINITY; }
if (indexB < 0) { indexB = Number.POSITIVE_INFINITY; }
return indexA - indexB;
};
}
const buttons = this.$params.buttons;
const buttonsElement = findButtonsElement(this.$dialogJq);
buttons.sort(sort);
buttonsElement[0].replaceChildren(...buttons.map((btn) => btn.jq[0]));
};
/**
* Return the internal jQuery wrapped DOM element for the entire Dialog.
*
* @returns {JQuery} the Dialog's jQuery DOM object.
*/
Dialog.prototype.jq = function jq() {
return this.$dialogJq;
};
/**
* If this dialog has content, return the jQuery DOM wrapper for it.
*
* @returns {JQuery} The DOM wrapper for the content. This wrapper will
* be empty if the dialog is shown with no content.
*/
Dialog.prototype.content = function () {
return findContentElement(this.$dialogJq);
};
/**
* If this dialog has a header, return the jQuery DOM wrapper for it.
*
* @returns {JQuery} The DOM wrapper for the header. This wrapper will
* be empty if the dialog is shown with no header.
* @since Niagara 4.12
*/
Dialog.prototype.header = function () {
return findHeaderElement(this.$dialogJq);
};
/**
* Return a promise for the dialog that will be resolved when the dialog closes.
* This is useful when wanting to use dialogs in a promise chain when creating a user interface.
*
* @returns {Promise} The promise to be resolved.
*/
Dialog.prototype.promise = function () {
return this.$df.promise;
};
/**
* An Object that defines a Button.
*
* @typedef {Object} module:dialogs~Button
* @inner
* @public
*
* @see module:dialogs~Dialog#on
*
* @property {String} [name] A button's unique name so it can be identified.
* @property {String} [displayName] The button's display name. This name will used
* on the button. If not specified, the button's name will be used instead.
* @property {Function} [handler] A function that will be invoked when the button is
* clicked. Returning false will keep the dialog from closing after this handler
* has been invoked.
* @property {boolean} [disable] if set to true, the button will be disabled until `enableButton`
* is called.
* @property {boolean} [hide] if set to true, the button will be hidden until `showButton` is
* called.
* @property {Boolean} [esc] If true, this handler will be invoked when the user
* hits the escape key.
*/
////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////
/**
* Create stunning, modal Dialog boxes in JavaScript.
*
* This is a UI library used to create dynamic, modal Dialog boxes in your
* browser.
*
* @example
* <caption>
* A simple modal OK dialog box.
* </caption>
* dialogs.showOk("Some Dialog Box Content")
* .ok(function () {
* console.log("The OK button has been clicked");
* });
*
* @example
* <caption>
* A Dialog box with a title and some HTML content.
* </caption>
* dialogs.showOk({
* title: "The Dialog Box's Title!",
* content: "<p>Some HTML Content</p>"
* })
* .ok(function () {
* console.log("The OK button has been clicked");
* });
*
* @example
* <caption>
* A simple Yes, No, Cancel dialog box.
* </caption>
* dialogs.showYesNoCancel("Would you like some tea with that?")
* .yes(function () {
* console.log("The user clicked Yes");
* })
* .no(function() {
* console.log("The user clicked No");
* })
* .cancel(function () {
* console.log("The user clicked Cancel");
* });
*
* @example
* <caption>
* Show a loading dialog box and have it close after the AJAX call has finished.
* </caption>
* dialogs.showLoading(0, $.ajax(uri, options));
*
* @example
* <caption>
* Use promises to show a loading dialog box and then pop up another dialog.
* </caption>
* var dlg = dialogs.showLoading();
* // After 2 seconds, close the loading box.
* setTimeout(function () {
* dlg.close();
* }, 2000);
* dlg.promise().then(([ dlg, buttonClicked ]) => {
* // Prints 'ok'
* console.log(buttonClicked);
*
* dialogs.showOk("The foobar has finished loading!");
* });
*
* @example
* <caption>
* Show a dialog. Have the content dynamically created by
* passing in a function for the content.
* </caption>
* dialogs.show(function(dlg, jq) {
* jq.html("<div>I love Niagara 4!</div>");
* });
*
* @example
* <caption>
* Show a dialog. Have the content dynamically created
* by passing in a function for the content. The dialog
* will only show when the return promise has been resolved.
* </caption>
* dialogs.show(function(dlg, jq) {
* return Promise.resolve($.ajax("/myajax")
* .then(function (response) {
* jq.html("The answer is..." + JSON.parse(response).answer);
* });
* });
*
* @example
* <caption>
* A Dialog BOX with background privacy setting.
* </caption>
* dialogs.showOk({
* title: "The Dialog Box's Title!",
* content: "<p>Some HTML Content</p>",
* private: true //ensures background contents are screened when the dialog is showing
* })
* .ok(function () {
* console.log("The OK button has been clicked");
* });
*
*
* @module dialogs
* @requires jquery
* @requires lex!js
* @requires css!nmodule/js/rc/dialogs/dialogs
*/
const dialogs = {
/**
* Create a Dialog box and return it.
*
* Please note, this will not show the Dialog box but just return
* an instance of a new one.
*
* This method can take either an Object Literal for parameters or a
* singular String argument for the Dialog's Content.
*
* @see module:dialogs~Button
*
* @param {Object|String|Function} params parameters for launching a Dialog,
* or directly, the `text` to be shown.
* @param {String} [params.text] the Dialog's content as a string of plain
* text. This string will be escaped to remove any unsafe HTML content before
* being displayed to the user. If you require HTML content in your dialog,
* use the "content" parameter instead.
* @param {String|Function} [params.content] the Dialog's content as HTML.
* Please note, if what is to be shown is text and not HTML, use of the text parameter SHOULD
* be used. If content is used it should be known safe HTML. This can also be a function used
* to generate the content dynamically. The callback function is passed the
* Dialog instance and content jQuery element as parameters. The callback
* can return a promise that when resolved will show the dialog box. By
* default, if a promise is returned, a loading dialog box will appear.
* @param {String} [params.title] the Dialog's title. This should not contain HTML as any HTML will be escaped.
* @param {Boolean} [params.fade] if true, the Dialog box will fade in
* quickly.
* @param {Function} [params.layout] an optional callback function to be
* called when the dialog lays itself out. It will receive the
* {@link module:dialogs~Dialog dialog} instance as the first parameter.
* If your content needs to perform some logic to lay itself out when the
* dialog changes dimensions, use this callback to do so.
* @param {Function} [params.ok] handler to be invoked when the 'OK' button
* is clicked. By defining this handler, this will also cause an 'OK' button
* to be added to the Dialog box.
* @param {Function} [params.cancel] handler to be invoked when the 'Cancel'
* button is clicked. By defining this handler, this will also cause a
* 'Cancel' button to be added to the Dialog box.
* @param {Function} [params.yes] handler to be invoked when the 'Yes'
* button is clicked. By defining this handler, this will also cause a 'Yes'
* button to be added to the Dialog box.
* @param {Function} [params.no] handler to be invoked when the 'No' button
* is clicked. By defining this handler, this will also cause a 'No' button
* to be added to the Dialog box.
* @param {Array.<module:dialogs~Button>} [params.buttons] an array of additional
* buttons.
* @param {JQuery} [params.parent] A parent jQuery wrapped DOM element to
* attach the dialog to. If not specified, the dialog is attached to the
* HTML document's body element.
* @param {Boolean} [params.private] Defaults to false. If true, the background
* contents are not visible when the dialog is showing.
* @returns {module:dialogs~Dialog}
* @throws {Error} the error generated by new Dialog if both a text and content parameter
* is provided
*
* @example
* <caption>Make a dialog box and show it</caption>
* dialogs.make("A dialog with no buttons")
* .show();
*/
make: function make(params) {
return new Dialog(params);
},
/**
* Create and show a Dialog box. Shortcut for `dialogs.make(params).show()`.
*
* @see module:dialogs.make
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @returns {module:dialogs~Dialog}
*
* @example
* <caption>
* Show a simple Dialog box.
* </caption>
* dialogs.show({
* title: "Dialog",
* content: "Hey this is a Dialog!",
* ok: function () {
* console.log("The OK button has been clicked");
* }
* });
*
* dialogs.show(function(dlg, jq) {
* jq.html("<div>I love Niagara 4!</div>");
* });
*
* dialogs.show(function(dlg, jq) {
* return Promise.resolve($.ajax("/myajax")
* .then(function (response) {
* jq.html("The answer is..." + JSON.parse(response).answer);
* });
* });
*/
show: function show(params) {
params = objectify(params, "text");
const { content, delay } = params;
var func,
loading = false,
dlg,
promise;
if (typeof content === "function") {
func = content;
// When a callback is used to generate the content, the loading dialog is on
// by default if the callback returns a promise.
loading = params.loading || params.loading === undefined;
delete params.loading;
}
dlg = dialogs.make(params);
if (func) {
promise = func(dlg, dlg.content());
// If the callback function returns a promise then
// make sure the dialog is shown when it resolves.
if (promise && typeof promise.then === "function") {
promise.then(function () {
dlg.show();
})
.catch(dialogs.showOk);
// If a promise is returned and we can show the loading dialog
// then show it.
if (loading) {
dialogs.showLoading(delay || 0, promise);
}
} else {
dlg.show();
}
} else {
dlg.show();
}
return dlg;
},
/**
* Iterate through each Dialog box.
*
* @param {Function} func called for each Dialog box whereby
* the first argument is index of the Dialog box with the second
* being the Dialog instance.
*/
each: function each(func) {
openDialogs.slice().forEach((dlg, i) => func(i, dlg));
},
/**
* Return the number of Open Dialogs (includes hidden).
*
* @return {Number} the number of open Dialogs.
*/
size: function size() {
return openDialogs.length;
},
/**
* Creates and shows a Dialog box with an OK button.
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @see module:dialogs.make
* @returns {module:dialogs~Dialog}
*/
showOk: function showOk(params) {
return showWithButtons(params, 'ok');
},
/**
* Creates and shows a Dialog box with OK and Cancel buttons.
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @see module:dialogs.make
* @returns {module:dialogs~Dialog}
*/
showOkCancel: function showOkCancel(params) {
return showWithButtons(params, 'ok', 'cancel');
},
/**
* Creates and shows a Dialog box with a Cancel button.
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @see module:dialogs.make
* @returns {module:dialogs~Dialog}
*/
showCancel: function showCancel(params) {
return showWithButtons(params, 'cancel');
},
/**
* Creates and shows a Dialog box with Yes and No buttons.
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @see module:dialogs.make
* @returns {module:dialogs~Dialog}
*/
showYesNo: function showYesNo(params) {
return showWithButtons(params, 'yes', 'no');
},
/**
* Creates and shows a Dialog box with Yes, No and Cancel buttons.
*
* @param {Object} params parameters for launching a Dialog. See
* {@link module:dialogs.make}.
*
* @see module:dialogs.make
* @returns {module:dialogs~Dialog}
*/
showYesNoCancel: function showYesNoCancel(params) {
return showWithButtons(params, 'yes', 'no', 'cancel');
},
/**
* Creates and shows a Loading Dialog box.
*
* @param {Object|Number} [params] Starting in Niagara 4.13 if an object literal is passed, this provides
* all the parameters for the function. If this is a Number, it's used as the delay.
* @param {Number} [params.delay] the delay (in milliseconds) before the
* Dialog box appears. By default, there is no delay in showing the Dialog.
* @param {Promise} [params.promise] An optional Promise that can be passed
* into the loading Dialog box. If defined, the loading Dialog box will
* automatically close after the promise has completed.
* @param {JQuery} [params.parent] The DOM parent to attach the dialog to. If not specified,
* the dialog is attached to the HTML page's body.
* @param {String} [params.loaderImg] an alternate loader image to use that is a URL.
* @param {String} [params.loaderTxt] an optional message to show while the dialog is loading.
* @param {String} [params.title] an optional title to show while the dialog is loading.
* @param {Array.<module:dialogs~Button|string>} [params.buttons] optional buttons to add to the loading dialog.
* This can be helpful if you want to make a cancelable loading dialog.
* @param {Promise} [promise] An optional Promise that can be passed
* into the loading Dialog box. If defined, the loading Dialog box will
* automatically close after the promise has completed.
* @param {JQuery} [parent] The DOM parent to attach the dialog to. If not specified,
* the dialog is attached to the HTML page's body.
* @param {String} [loaderImg] an alternate loader image to use that is a URL.
* @returns {module:dialogs~Dialog}
*
* @example
* <caption>
* Show a loading Dialog box if an AJAX call takes longer than
* half a second to complete.
* </caption>
*
* // Make an AJAX call that may take longer that half a second.
* var promise = $.ajax({
* url: "test.html",
* context: document.body
* }).done(function() {
* $(this).addClass("done");
* });
*
* // If the AJAX call takes longer that half a second, display a
* // Loading Dialog box.
* dialogs.showLoading(500, promise);
*
* <caption>
* Show a loading Dialog box with an OK button that can be pressed to let work continue in the background.
* </caption>
* dialogs.showLoading({
* promise,
* loaderTxt: "Running Job now. Click OK to allow this to be completed in the background.",
* buttons: [
* {
* name: "ok",
* handler: function (dialog) {
* dialog.close();
* }
* }
* ]
* });
*/
showLoading: function showLoading(params, promise, parent, loaderImg) {
let delay, buttons, loaderTxt, title;
if (typeof params === 'number') {
delay = params;
} else if (params) {
promise = params.promise;
parent = params.parent;
loaderImg = params.loaderImg;
buttons = params.buttons;
loaderTxt = params.loaderTxt;
title = params.title;
}
loaderImg = loaderImg || defaultLoadImg;
let content = loaderImg;
if (loaderTxt) {
content = { loaderImg, loaderTxt };
}
// Ensure the image is loaded before we display the loading dialog
var img,
dlg = dialogs.make({
title: title || jsLex.get("dialogs.loading"),
content,
parent: parent,
loading: true,
fade: true,
buttons
});
delay = delay || 0;
img = $(".js-dialog-loading-img", dlg.$dialogJq)[0];
img.onload = function () {
if (!dlg.$closed && !dlg.$hidden) {
// Lazily initialize the content once the image has loaded...
dlg.$delayId = setTimeout(function () {
dlg.show();
}, delay);
}
};
// If a promise is passed in then automatically close the
// dialog box when it's resolved or rejected.
if (promise) {
promise.then(function () {
dlg.close();
}, function (err) {
dlg.close("cancel", err || unknownErr);
});
}
img.src = loaderImg;
return dlg;
}
};
function showWithButtons(params, ...buttons) {
params = objectify(params, "text");
buttons.forEach((buttonName) => { params[buttonName] = params[buttonName] || defaultButtonHandler; });
return dialogs.show(params);
}
////////////////////////////////////////////////////////////////
// Handle key presses
////////////////////////////////////////////////////////////////
// Handle any key presses
$(function handleKeyPresses() {
$(document).on("keyup", function (e) {
var esc = e.keyCode === /*esc key*/27,
buttons,
bt,
i,
x;
if (esc && openDialogs.length > 0) {
for (i = openDialogs.length - 1; i >= 0; --i) {
// Find the first dialog that's not hidden or closed.
if (openDialogs[i].$dialogJq &&
!openDialogs[i].isClosed() &&
!openDialogs[i].isHidden()) {
buttons = openDialogs[i].$params.buttons;
if (esc) {
// Invoke any buttons that handle an escape press
for (x = 0; x < buttons.length; ++x) {
bt = buttons[x];
if (bt.esc && canClick(bt.jq)) {
bt.jq.trigger('click');
}
}
}
break;
}
}
}
});
});
return dialogs;
});