/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* An Alfresco styled dialog. Extends the default Dojo dialog by adding support for a row of buttons defined
* by the [widgetButtons]{@link module:alfresco/dialogs/AlfDialog#widgetButtons} attribute. The main body
* of the dialog can either be defined as simple text assigned to the
* [content]{@link module:alfresco/dialogs/AlfDialog#content} attribute or as a JSON model assigned to
* the [widgetsContent]{@link module:alfresco/dialogs/AlfDialog#widgetsContent} attribute (widgets take
* precedence over text - it is not possible to mix both).
*
* @module alfresco/dialogs/AlfDialog
* @extends external:dijit/Dialog
* @mixes module:alfresco/core/Core
* @mixes module:alfresco/core/CoreWidgetProcessing
* @author Dave Draper
*/
/**
* AlfDialog is based on dijit/Dialog.
*
* @external dijit/Dialog
* @see http://dojotoolkit.org/reference-guide/1.9/dijit/Dialog.html
*/
define(["dojo/_base/declare",
"dijit/Dialog",
"alfresco/core/Core",
"alfresco/core/CoreWidgetProcessing",
"alfresco/core/ResizeMixin",
"alfresco/core/topics",
"dijit/_FocusMixin",
"dojo/_base/lang",
"dojo/sniff",
"dojo/_base/array",
"dojo/dom-construct",
"dojo/dom-class",
"dojo/dom-style",
"dojo/dom-geometry",
"dojo/html",
"dojo/aspect",
"dojo/on",
"dojo/when",
"jquery",
"alfresco/layout/SimplePanel"],
function(declare, Dialog, AlfCore, CoreWidgetProcessing, ResizeMixin, topics, _FocusMixin, lang, sniff, array,
domConstruct, domClass, domStyle, domGeom, html, aspect, on, when, $) {
return declare([Dialog, AlfCore, CoreWidgetProcessing, ResizeMixin, _FocusMixin], {
/**
* An array of the CSS files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{cssFile:"./css/AlfDialog.css"}]
*/
cssRequirements: [{cssFile:"./css/AlfDialog.css"}],
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/AlfDialog.properties"}]
*/
i18nRequirements: [{i18nFile: "./i18n/AlfDialog.properties"}],
/**
* A scope against which to publish the cancellation topic
*
* @instance
* @type {string}
* @default
*/
cancelPublishScope: null,
/**
* An optional topic to be published when the dialog is cancelled (escape keypress or cross-button click)
*
* @instance
* @type {string}
* @default
*/
cancelPublishTopic: null,
/**
* Basic text content to be added to the dialog.
*
* @instance
* @type {String}
* @default
*/
content: "",
/**
* This controls the duration of the fade in/out effect when the dialog is shown and hidden.
*
* @instance
* @type {number}
* @default
* @since 1.0.33
*/
duration: 0,
/**
* If this is set to true then the dialog will retain it's opening width regardless of what happens
* to it's contents. This is especially useful when the dialog contains widgets that resize themselves
* that could result in the dialog shrinking (this can occur when using
* [HorizontalWidgets]{@link module:alfresco/layout/HorizontalWidgets}.
*
* @instance
* @type {boolean}
* @default
*/
fixedWidth: false,
/**
* Indicates whether or not to override all dimension settings and to make the dialog consume all
* the available space on the screen (minus any [padding]{@link module:alfresco/dialogs/AlfDialog#fullScreenPadding}).
*
* @instance
* @type {boolean}
* @default
* @since 1.0.35
*/
fullScreenMode: false,
/**
* When [full screen mode]{@link module:alfresco/dialogs/AlfDialog#fullScreenMode} is used this is the value in pixels
* that will be left as a padding between the edge of the dialog and the edge the screen.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.35
*/
fullScreenPadding: 10,
/**
* In some cases the content placed within the dialog will handle overflow itself, in that
* case this should be set to false. However, in most cases the dialog will want to manage
* overflow itself. Effectively this means that scroll bars will be added as necessary to
* ensure that the user can see all of the dialog content.
*
* @instance
* @type {boolean}
* @default
*/
handleOverflow: true,
/**
* Indicates the the default minimum width rules for the dialog (controlled through LESS
* variables) can be ignored.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.84
*/
noMinWidth: false,
/**
* A placeholder for the resize-listener that's enabled while the dialog is visible. This
* value is set automatically.
*
* @instance
* @readonly
* @type {object}
* @since 1.0.43
*/
resizeListener: null,
/**
* Widgets to be processed into the main node
*
* @instance
* @type {Object[]}
* @default
*/
widgetsContent: null,
/**
* Widgets to be processed into the button bar
*
* @instance
* @type {Object[]}
* @default
*/
widgetsButtons: null,
/**
* Extends the default constructor to adjust for changes between 1.9.x and 1.10.x versions
* of the dijit/Dialog. This has been done to keep AlfDialog configuration backwards compatible.
*
* @instance
* @param {object} args The constructor arguments
*/
constructor: function alfresco_dialogs_AlfDialog__constructor(args) {
if (!args.content && args.textContent)
{
args.content = args.textContent;
delete args.textContent;
}
if (args.content)
{
args.content = this.encodeHTML(args.content);
}
declare.safeMixin(args);
},
/**
* Calculates various heights that are used to set dimensions of the dialog when it is created
* as well as on resize events (such as resizing the window).
*
* @instance
* @returns {object} heights The calculated heights
* @returns {number} heights.clientHeight The height of the viewport
* @returns {number} heights.documentHeight The height of the document
* @returns {number} heights.maxBodyHeight The maximum height allowed for the body of the dialog
* @returns {number} heights.paddingAdjustment The pixels to allow for padding in the dialog body
* @returns {number} heights.simplePanelHeight The height of the [SimplePanel]{@link module:alfresco/layout/SimplePanel} containing the dialog content
* @since 1.0.36
*/
calculateHeights: function alfresco_dialogs_AlfDialog__calculateHeights() {
var calculatedHeights = {};
var docHeight = $(document).height(),
clientHeight = $(window).height();
var h = (docHeight < clientHeight) ? docHeight : clientHeight;
// We want to ensure that the dialog always fits within the viewport, so take the available
// height and remove 200 pixels to accomodate both the title and buttons bars and still leave
// some padding...
var maxHeight = h - 200;
// By default there is padding around the dialog body (12px above and below the body)...
// We need to take this into consideration when calculating the simple panel height to ensure
// that it fits perfectly within the available space...
var paddingAdjustment = 24;
if (this.additionalCssClasses && this.additionalCssClasses.indexOf("no-padding") !== -1)
{
paddingAdjustment = 0;
}
var simplePanelHeight = null;
if (this.contentHeight)
{
simplePanelHeight = (Math.min(maxHeight, parseInt(this.contentHeight, 10)) - paddingAdjustment) + "px";
}
calculatedHeights.documentHeight = docHeight;
calculatedHeights.clientHeight = clientHeight;
calculatedHeights.maxBodyHeight = maxHeight;
calculatedHeights.paddingAdjustment = paddingAdjustment;
calculatedHeights.simplePanelHeight = simplePanelHeight;
return calculatedHeights;
},
/**
* Returns a promise to provide an array of all the buttons in the dialog.
*
* @instance
* @return {promise} An array of the buttons widgets for the dialog
* @since 1.0.58
*/
getButtons: function alfresco_dialogs_AlfDialog__getButtons() {
return this.getProcessedWidgets("BUTTONS");
},
/**
* Extends the superclass implementation to set the dialog as not closeable (by clicking an "X"
* in the corner).
*
* @instance
*/
postMixInProperties: function alfresco_dialogs_AlfDialog__postMixInProperties() {
this.inherited(arguments);
// TODO: Had to use an existing NLS message for point of fix during dev-cycle - needs own widget NLS prop
this.buttonCancel = this.message("button.close");
},
/**
* Extends the superclass implementation to process the widgets defined by
* [widgetButtons]{@link module:alfresco/dialogs/AlfDialog#widgetButtons} into the buttons bar
* and either the widgets defined by [widgetsContent]{@link module:alfresco/dialogs/AlfDialog#widgetsContent}
* or the text string set as [content]{@link module:alfresco/dialogs/AlfDialog#content} into
* the main body of the dialog.
*
* @instance
*/
postCreate: function alfresco_dialogs_AlfDialog__postCreate() {
// jshint maxcomplexity:false, maxstatements:false
this.inherited(arguments);
// Listen for requests to resize the dialog...
this.alfSubscribe("ALF_RESIZE_DIALOG", lang.hitch(this, this.onResizeRequest));
domClass.add(this.domNode, "alfresco-dialog-AlfDialog");
// Add in any additional CSS classes...
if (this.additionalCssClasses)
{
domClass.add(this.domNode, this.additionalCssClasses);
}
if (this.noMinWidth)
{
domClass.add(this.domNode, "alfresco-dialog-AlfDialog--no-min-width");
}
// Set a width for the dialog
if (this.dialogWidth)
{
domStyle.set(this.domNode, {
width: this.dialogWidth
});
}
// Calculate the heights required for the dialog...
var calculatedHeights = this.calculateHeights();
domConstruct.empty(this.containerNode);
this.bodyNode = domConstruct.create("div", {
"class" : "dialog-body",
style: "max-height:" + calculatedHeights.maxBodyHeight + "px"
}, this.containerNode, "last");
// Workout a maximum height for the dialog as it should always fit in the window...
// Set the dimensions of the body if required...
domStyle.set(this.bodyNode, {
width: this.contentWidth ? this.contentWidth: null,
height: this.contentHeight ? this.contentHeight: null
});
if (sniff("ie") === 8 || sniff("ie") === 9)
{
// Add specific classes for IE8 and 9 to undo the CSS calculations and selectors
// that make the footer always visible (because they don't support CSS calc)...
domClass.add(this.domNode, "iefooter");
}
// It is important to create the buttons BEFORE creating the main body. This is especially important
// for when the buttons will respond to initial setup events from a form placed inside the body (e.g.
// so that the buttons are disabled initially if required)
if (this.widgetsButtons)
{
this.buttonsNode = domConstruct.create("div", {
"class" : "footer"
}, this.containerNode, "last");
this.processWidgets(this.widgetsButtons, this.buttonsNode, "BUTTONS");
}
else
{
domClass.add(this.bodyNode, "no-buttons");
}
if (this.widgetsContent)
{
var bodyModel = [{
name: "alfresco/layout/SimplePanel",
assignTo: "_dialogPanel",
config: {
handleOverflow: this.handleOverflow,
height: !this.handleOverflow && this.contentHeight ? "100%" : calculatedHeights.simplePanelHeight,
widgets: this.widgetsContent
}
}];
this.processWidgets(bodyModel, this.bodyNode, "BODY");
}
else if (this.content)
{
// Add basic text content into the container node. An example of this would be for
// setting basic text content in an confirmation dialog...
html.set(this.bodyNode, this.content);
}
this.alfSetupResizeSubscriptions(this.onWindowResize, this);
},
/**
* Called when the dialog is shown.
* Disable the outer page scrolling ability by the user when a dialog is showing.
*
* @instance
*/
onShow: function alfresco_dialogs_AlfDialog__onShow() {
this.inherited(arguments);
// Publish events if the dialog moves
if(this._moveable) {
aspect.after(this._moveable, "onMoveStart", lang.hitch(this, function(returnVal, /*jshint unused:false*/ originalArgs) {
this.alfPublish("ALF_DIALOG_MOVE_START", null, true);
return returnVal;
}));
aspect.after(this._moveable, "onMoveStop", lang.hitch(this, function(returnVal, /*jshint unused:false*/ originalArgs) {
this.alfPublish("ALF_DIALOG_MOVE_STOP", null, true);
return returnVal;
}));
}
// Listen to resize events
this.resizeListener = this.addResizeListener(this.containerNode, this.domNode.parentNode);
// See AKU-604 - ensure that first item in dialog is focused...
// Moved for AKU-711 because _onFocus was not always being called...
if (this._dialogPanel)
{
when(this._dialogPanel.getProcessedWidgets(), lang.hitch(this, function(children) {
array.some(children, function(child) {
var focused = false;
if (typeof child.focus === "function")
{
child.focus();
focused = true;
}
return focused;
});
}));
}
},
/**
* Override the onCancel method of the Dojo Dialog class
*
* @instance
* @override
*/
onCancel: function alfresco_dialogs_AlfDialog__onCancel() {
this.inherited(arguments);
if (this.cancelPublishTopic) {
this.alfPublish(this.cancelPublishTopic, null, false, false, this.cancelPublishScope);
}
},
/**
* Called when the dialog is hidden.
* Enable the outer page scrolling - disabled in onShow().
*
* @instance
*/
onHide: function alfresco_dialogs_AlfDialog__onHide() {
this.inherited(arguments);
domClass.remove(this.domNode, "dialogDisplayed");
domClass.add(this.domNode, "dialogHidden");
if (this.resizeListener) {
this.resizeListener.remove();
this.resizeListener = null;
}
},
/**
* This is called whenever the window is resized. It ensures that the dialog body is the correct height
* when taking into account the new size of the view port.
*
* @instance
* @since 1.0.36
*/
onWindowResize: function alfresco_dialogs_AlfDialog__onWindowResize() {
var calculatedHeights = this.calculateHeights();
if (calculatedHeights.maxBodyHeight && !this.fullScreenMode)
{
// Don't set a max-height when it's 0 or when in full screen mode...
domStyle.set(this.bodyNode, {
"max-height": calculatedHeights.maxBodyHeight + "px"
});
}
this.resize();
},
/**
* This is called once the dialog gets focus and at that point it is necessary to resize
* it's contents as this is the final function that is called after the dialog is displayed
* and therefore we know it will have dimensions to size against.
*
* @instance
*/
_onFocus: function alfresco_dialogs_AlfDialog___onFocus() {
this.inherited(arguments);
var computedStyle = domStyle.getComputedStyle(this.containerNode);
var output = domGeom.getMarginBox(this.containerNode, computedStyle);
if (this.handleOverflow === true)
{
domClass.add(this.domNode, "handleOverflow");
}
if (this.fixedWidth === true)
{
// Fix the width of the dialog - this has been done to prevent the dialog from shrinking
// as its contents are resized on window resize events. The issue here is that the dialog
// may become too big for the initial window, but that's preferable to shrinkage...
domStyle.set(this.domNode, "width", output.w + "px");
}
this.alfPublishResizeEvent(this.domNode);
domClass.remove(this.domNode, "dialogHidden");
// Publish the widgets ready
this.alfPublish(topics.PAGE_WIDGETS_READY, {}, true);
// NOTE: This is duplicated from the onShow function to absolutely be sure that focus
// is given to child widgets. It was found in development that in particular
// when attempting to focus on the TinyMCE editor that the carat was not being
// displayed for Chrome without this additional code. It was noted that Firefox,
// Chrome and IE all behaved slightly differently with regards to this function
// being called. Although inefficient, it is at least reliable.
if (this._dialogPanel)
{
when(this._dialogPanel.getProcessedWidgets(), lang.hitch(this, function(children) {
array.some(children, function(child) {
var focused = false;
if (typeof child.focus === "function")
{
child.focus();
focused = true;
}
return focused;
});
}));
}
domClass.add(this.domNode, "dialogDisplayed");
},
/**
* Extends the default resize function to to provide the
* [full screen mode]{@link module:alfresco/dialogs/AlfDialog#fullScreenMode} capability.
*
* @instance
* @since 1.0.35
*/
resize: function alfresco_dialogs_AlfDialog__resize() {
if (this.fullScreenMode === true)
{
var dimensionAdjustment = this.fullScreenPadding * 2;
this.inherited(arguments, [{
t: this.fullScreenPadding,
l: this.fullScreenPadding,
w: $(window).width() - dimensionAdjustment,
h: $(window).height() - dimensionAdjustment
}]);
// When in full screen mode it is also necessary to take care of the inner dimensions
// of the dialog...
var calculatedHeights = this.calculateHeights();
var containerHeight = $(this.containerNode).height();
var bodyHeight = containerHeight;
if (this.widgetsButtons)
{
// Deduct height for the widgets buttons if present
bodyHeight = bodyHeight - 44;
}
$(this.bodyNode).height(bodyHeight);
$(this.bodyNode).css("max-height", bodyHeight); // NOTE: This is necessary to override the default max-height
if (this._dialogPanel)
{
$(this._dialogPanel.domNode).height(bodyHeight - calculatedHeights.paddingAdjustment);
}
}
else
{
// See AKU-1162
// This addresses a Chrome specific issue that causes a render error with where the footer
// bar gets placed...
if (this.buttonsNode)
{
domStyle.set(this.buttonsNode, "display", "none");
window.requestAnimationFrame(lang.hitch(this, function() {
domStyle.set(this.buttonsNode, "display", "block");
}));
}
this.inherited(arguments);
}
},
/**
* Override the default dialog method to ensure that the dialog starts its position
* at the top of the page to avoid the page scrolling to focus on its content (see
* call to child.focus() below).
*
* @instance
* @override
* @returns {Promise} Returns the superclass' promise
* @since 1.0.63
*/
show: function alfresco_dialogs_AlfDialog__show() {
domStyle.set(this.domNode, {
top: (document.body.scrollTop || document.documentElement.scrollTop) + "px"
});
return this.inherited(arguments);
},
/**
* Calls the resize() function
*
* @instance
* @param {object} payload
*/
onResizeRequest: function alfresco_dialogs_AlfDialog__onResizeRequest(payload) {
// jshint unused:false
if (this.domNode)
{
this.onWindowResize();
}
},
/**
* Iterates over any buttons that are created and calls the [attachButtonHandler]{@link module:alfresco/dialogs/AlfDialog#attachButtonHandler}
* function with each of them to ensure that clicking a button always results in the dialog being hidden. It is up to the
* buttons defined to publish a request to perform the appropriate action.
*
* @instance
* @param {Object[]}
*/
allWidgetsProcessed: function alfresco_dialogs_AlfDialog__allWidgetsProcessed(widgets, processWidgetsId) {
if (processWidgetsId === "BUTTONS")
{
// When creating the buttons, attach the handler to each created...
this._buttons = [];
array.forEach(widgets, lang.hitch(this, this.attachButtonHandler));
// See AKU-1116...
// Forms support the ability to submit on the publication of a topic, but form
// dialogs hide the standard form buttons. This section of code will handle events
// emitted by forms when no form buttons can be found. It allows the form dialogs
// to be submitted on enter...
on(this.domNode, "onFormSubmit", lang.hitch(this, function() {
if (this._buttons)
{
array.some(this._buttons, function(button) {
if (domClass.contains(button.domNode, "confirmationButton"))
{
button.activate();
return true;
}
return false;
});
}
}));
}
else
{
// Once all the content is created the widget instances are added to the publish payload
// of all the buttons. This is done so that the dialog content is always included in publish
// requests. It is important NOT to override the default payload...
array.forEach(this._buttons, function(button) {
if (!button.publishPayload)
{
button.publishPayload = {};
}
// The dialog content is expected to be found in the SimplePanel that is the main content
// widget. It's array of processed widgets represent the dialog content that should be worked with
if (widgets && widgets[0] && widgets[0]._processedWidgets)
{
button.publishPayload.dialogContent = widgets[0]._processedWidgets;
}
else
{
button.publishPayload.dialogContent = [];
}
button.publishPayload.dialogRef = this; // Add a reference to the dialog itself so that it can be destroyed
}, this);
}
},
/**
* This field is used to keep track of the buttons that are created.
*
* @instance
* @type {object[]}
* @default
*/
_buttons: null,
/**
* An optional array of CSS classes to check buttons for that will suppress dialog closure when
* the button is clicked. This has been added to allow button actions to be carried out to a
* successful conclusion before the dialog is closed. This means that button action failures
* will leave the dialog visible. In particular this addresses the use case of form dialogs
* not being closed if the form submission fails.
*
* @instance
* @type {array}
* @default
*/
suppressCloseClasses: null,
/**
* Hitches each button click to the "hide" method so that whenever a button is clicked the dialog will be hidden.
* It's assumed that the buttons will take care of their own business.
*
* @instance
* @param {Object} widget The widget update
* @paran {number} index The index of the widget in the widget array
*/
attachButtonHandler: function alfresco_dialogs_AlfDialog__attachButtonHandler(widget, index) {
// jshint unused:false
var disableClose = false;
if (this.suppressCloseClasses)
{
disableClose = array.some(this.suppressCloseClasses, function(className) {
return domClass.contains(widget.domNode, className);
}, this);
}
if (widget)
{
this._buttons.push(widget); // Add the button so we can add the content to them later...
if (!disableClose)
{
if (sniff("ie") === 8)
{
// Need to use "after" rather than "before" for IE8...
aspect.after(widget, "onClick", lang.hitch(this, this.hide));
}
else
{
aspect.before(widget, "onClick", lang.hitch(this, this.hide));
}
}
}
},
/**
* Set the title of the dialog. The Dojo way of doing it uses innerHTML, so this is the recommended, XSS-safe way of doing it.
*
* @instance
* @param {string} newTitle The new title
* @since 1.0.52
*/
setTitle: function alfresco_dialogs_AlfDialog__setTitle(newTitle) {
this.titleNode.textContent = newTitle;
}
});
});