/* global define, $, window, TweenLite, TimelineLite, i18n, console, jQuery, RAMP */
/*jslint white: true */
/**
* UI submodule
*
* @module RAMP
* @submodule UI
* @main UI
*/
/**
* A class for handling most of the GUI on the page.
*
* ####Imports RAMP Modules:
* {{#crossLink "GlobalStorage"}}{{/crossLink}}
* {{#crossLink "EventManager"}}{{/crossLink}}
* {{#crossLink "Theme"}}{{/crossLink}}
* {{#crossLink "Util"}}{{/crossLink}}
* {{#crossLink "Dictionary"}}{{/crossLink}}
* {{#crossLink "PopupManager"}}{{/crossLink}}
* {{#crossLink "TmplHelper"}}{{/crossLink}}
*
* ####Uses RAMP Templates:
* {{#crossLink "templates/sub_panel_template.json"}}{{/crossLink}}
*
* @class GUI
* @static
* @uses dojo/_base/array
* @uses dojo/topic
* @uses dojo/_base/lang
* @uses dojo/Deferred
* @uses dojo/domReady!
* @uses dojo/domReady!
*/
define([
// Dojo
"dojo/topic", "dojo/_base/lang", "dojo/Deferred",
// Ramp
"ramp/globalStorage", "ramp/eventManager",
"ramp/theme", "ramp/imageExport",
// Text
"dojo/text!./templates/sub_panel_template.json",
"dojo/text!./templates/datagrid_template.json",
// Util
"utils/util", "utils/dictionary", "utils/popupManager", "utils/tmplHelper",
// Dom Ready
"dojo/domReady!"
],
function (
// Dojo
topic, dojoLang, Deferred,
// Ramp
GlobalStorage, EventManager,
Theme, ImageExport,
// Text
subPanelTemplate,
datagridTemplate,
// Util
UtilMisc, utilDict, popupManager, TmplHelper) {
"use strict";
var jWindow = $(window),
sidePanelWbTabs = $("#panel-div > .wb-tabs"),
sidePanelTabList = sidePanelWbTabs.find(" > ul[role=tablist]"),
sidePanelTabPanels = sidePanelWbTabs.find(" > .tabpanels"),
mapContent = $("#mapContent"),
loadIndicator = mapContent.find("#map-load-indicator"),
subPanels = {},
subPanelLoadingAnimation,
// subPanelAttribute definition
/**
* A class holding properties of the SubPanel.
*
* @class SubPanelSettings
* @for SubPanel
*/
subPanelAttr = {
/**
* A name used to identify the subpanel being opened (e.g. "Details", "Metadata")
*
* @property panelName
* @for SubPanelSettings
* @type {String}
* @default ""
*/
panelName: "",
/**
* Title of the content to be displayed on the SubPanel (e.g. "CESI Water Quality Indicators")
*
* @property title
* @type {String}
* @default ""
*/
title: "",
/**
* The text inside the subpanel. Can be String or a jQuery object. All nodes sporting CSS class
* ".shorten-candidate" are treated to the shortening procedure - long strings are curtailed, and [more/less] links are placed at their ends.
*
* @property content
* @type {String | jObject}
* @default null
*/
content: null,
templateKey: "summary_sub_panel_container",
/**
* The node after which the panel will be inserted (e.g. node.find(".layer-details")).
*
* @property target
* @type {jObject}
* @default null
*/
target: null,
/**
* The name of the module that requested to open the SubPanel (e.g. "filterManager"). Used for identification of the panel's loyalty.
*
* @property origin
* @type {String}
* @default ""
*/
origin: "",
/**
* A unique id of the SubPanel. If none provided, a random one is generated. It is used to determine the animation and update function
* to run on content update.
*
* @property guid
* @type {String}
* @default ""
*/
guid: "",
/**
* Indicates that the open SubPanel request is a content update to the already opened SubPanel.
* Does not trigger any of the `doOn-` or `doAfter-` functions.
*
* __Use case:__ the uses clicks on the metadata button.
* 1. A request to open a SubPanel is sent with only the title since the metadata content is not yet available
* 2. Metadata is fetched from the server
* 3. A second request to open a SubPanel is sent having `update` set to `true` and featuring the __same__ `guid` as the first request
* 4. Only the content of the SubPanel is updated; no extra animations are triggered
*
* @property update
* @type {Boolean}
* @default false
*/
update: false,
/**
* The callback function when the panel starts the opening animation; also triggered by updating panel content; can be triggered many times.
*
* @property doOnOpen
* @type {Function}
* @default null
*/
doOnOpen: null,
/**
* The callback function when the panel finishes the opening animation; also triggered by updating panel content; can be triggered many times.
*
* @property doAfterOpen
* @type {Function}
* @default null
*/
doAfterOpen: null,
/**
* The callback function when the panel starts the closing animation; also triggered by updating panel content; can be triggered many times.
*
* @property doOnHide
* @type {Function}
* @default null
*/
doOnHide: null,
/**
* The callback function when the panel becomes hidden; also triggered by updating panel content; can be triggered many times.
*
* @property doAfterHide
* @type {Function}
* @default null
*/
doAfterHide: null,
/**
* The callback function when the panel is completely closed, its nodes destroyed; can be triggered only once in a lifespan of the panel.
*
* @property doOnDestroy
* @type {Function}
* @default null
*/
doOnDestroy: null,
/**
* The callback function executed after the SubPanel content is updated.
* __Doesn't work correctly yet.__
*
* @property doAfterUpdate
* @type {Function}
* @default null
*/
doAfterUpdate: null,
/**
* The target number of chars after which a content text node will be shortened.
*
* @property showChars
* @type {Number}
* @default 170
*/
showChars: 170
},
// Panel Prototype
/**
* [subPanelPrototype description]
*
* @class SubPanel
* @constructor
* @for GUI
*/
subPanelPrototype = {
/**
* Indicates if the closing animation is under way.
*
* @property _closing
* @private
* @for SubPanel
* @type {Boolean}
* @default false
*/
_closing: false,
/**
* Holds a deferred that would destroy the panel after the closing animation completes. May be interrupted.
*
* @property _destroyDeferred
* @type {Deferred}
* @private
* @default null
*/
_destroyDeferred: null,
/**
* SubPanel attributes
*
* @property _attr
* @private
* @default null
* @type {SubPanelSettings}
*/
_attr: null,
/**
* Indicates if the SubPanel is visible at the moment. Doesn't make the panel visible or invisible, just prevents animations on the content
* to run when it is set to `true`.
*
* @property _visible
* @private
* @default false
* @type {Boolean}
*/
_visible: false,
/**
* The outermost `div` of the SubPanel.
*
* @property container
* @default null
* @type {jObject}
*/
container: null,
/**
* The inner `div` of the SubPanel. Closing and opening animations are run on this `div`.
*
* @property panel
* @default null
* @type {jQobject}
*/
panel: null,
/**
* `div` housing the content of the SubPanel, including its title.
*
* @property _subPanelContentDiv
* @private
* @default null
* @type {jObject}
*/
_subPanelContentDiv: null,
/**
* Heading of the content in the SubPanel.
*
* @property _panelTitle
* @private
* @default null
* @type {jObject}
*/
_panelTitle: null,
/**
* `div` housing the content of the SubPanel, excluding its title.
*
* @property _panelContentDiv
* @private
* @default null
* @type {jObject}
*/
_panelContentDiv: null,
/**
* Default duration of the SubPanel animation in milliseconds.
*
* @property _animatePanelDuration
* @private
* @default 0.5
* @type {Number}
*/
_animatePanelDuration: 0.5, //0.4,
timeLine: null,
/**
* Apply the shortening plugin to the panel data
*
* @method parseContent
* @param {jObject} data Content to be shortened
* @return {jObject} Content with after shortening long text nodes
*/
parseContent: function (data) {
//console.log(this._attr.showChars, jQuery.type(data), data);
return (jQuery.type(data) === "object" ? data : $(data))
.find(".shorten-candidate").shorten({
showChars: this._attr.showChars
})
.removeClass("shorten-candidate").end();
},
/**
* Returns this SubPanel's settings object.
*
* @method getAttributes
* @return {SubPanelSettings} This SubPanel's settings
*/
getAttributes: function () {
return this._attr;
},
/**
* Returns this SubPanel's container `div`.
*
* @method getContainer
* @return {jObject} This SubPanel's `div`
*/
getContainer: function () {
return this.container;
},
/**
* Returns the inner `div` of the SubPanel
*
* @method getPanel
* @return {jObject} The inner `div` of the SubPanel
*/
getPanel: function () {
return this.panel;
},
/**
* Returns the `origin` of this SubPanel.
*
* @method getOrigin
* @return {String} The `origin` of this SubPanel
*/
getOrigin: function () {
return this._attr.origin;
},
/**
* Returns the `guid` of this SubPanel.
*
* @method getOrigin
* @return {String} The `guid` of this SubPanel
*/
getGuid: function () {
return this._attr.guid;
},
/**
* Destroys this SubPanel.
*
* @method destroy
* @param {Number} speed The duration of the animation in milliseconds
* @param {Deferred} deferred The deferred to be resolved after the SubPanel is destroyed
*/
destroy: function (speed, deferred) {
if (this._attr.doOnHide) {
this._attr.doOnHide();
}
this._closing = true;
this._destroyDeferred = deferred;
// remove CSS animation class to prevent flickering
this._subPanelContentDiv.find(".fadeInDown").removeClass("fadeInDown");
//sidePanel.getContainer().after(this.container);
layoutController.getPanelContainer().before(this.container);
//adjutSubPanelDimensions(this);
layoutController.subPanelChange(false, this._attr.origin, this.container, false);
// do after closing animation completes
this.timeLine.eventCallback("onReverseComplete",
function () {
if (this._attr.doAfterHide) {
this._attr.doAfterHide();
}
if (this._attr.doOnDestroy) {
this._attr.doOnDestroy();
}
this._visible = false;
layoutController.subPanelChange(false, this._attr.origin, null, true);
this.container.remove();
if (deferred) {
deferred.resolve(true);
}
}, [], this);
this.timeLine.reverse();
},
/**
* Reopens the SubPanel - stops the closing animation and initiates the opening animation.
*
* @method reopen
*/
reopen: function () {
//this.panel.stop();
this.timeLine.pause();
this._closing = false;
if (this._destroyDeferred) {
this._destroyDeferred.cancel();
this._destroyDeferred = null;
}
this.open();
},
/**
* Opens the SubPanel. Sends out `EventManager.GUI.SUBPANEL_CHANGE` event.
*
* @method open
*/
open: function () {
if (this._attr.doOnOpen) {
this._attr.doOnOpen();
}
this._visible = true;
layoutController.subPanelChange(true, this._attr.origin, this.container, false);
this.timeLine.play();
},
/**
* Assigns a new origin to the SubPanel.
*
* @method changeOrigin
* @param {String} newOrigin The new origin of the SubPanel.
*/
changeOrigin: function (newOrigin) {
this._attr.origin = newOrigin;
},
/**
* Shifts the SubPanel to the new node in the DOM.
*
* @method shiftTarget
* @param {jObject} newTarget A node in the DOM to shift the SubPanel to
*/
shiftTarget: function (newTarget) {
if (this._attr.target !== newTarget) {
// remove animation class to prevent flickering of data
this._subPanelContentDiv.find(".fadeInDown").removeClass("fadeInDown");
newTarget.after(this.container);
this._attr.target = newTarget;
}
},
/**
* Creates a new instance of SubPanel.
*
* @method create
* @param {SubPanelSettings} a Settings for the SubPanel
*/
create: function (a) {
var //subPanelContent,
subPanelString,
parsedContent;
console.log("create panel, ", a.origin);
a.guid = a.guid || UtilMisc.guid();
dojoLang.mixin(this._attr, a);
subPanelString = TmplHelper.template(this._attr.templateKey,
dojoLang.mixin(
this._attr,
{
closeTitle: i18n.t('gui.actions.close')
}
),
subPanelTemplate);
this.container = $(subPanelString).insertAfter(this._attr.target);
this.panel = this.container.find(".sub-panel");
this._subPanelContentDiv = this.panel.find(".sub-panel-content");
this._panelTitle = this.panel.find(".panel-title");
this._panelContentDiv = this.panel.find(".panel-content-div");
// set panel content
parsedContent = this.parseContent(this._attr.content);
this._panelContentDiv.empty().append(parsedContent);
this.timeLine = new TimelineLite(
{
paused: true,
onComplete: function () {
if (this._attr.doAfterOpen) {
this._attr.doAfterOpen();
}
layoutController.subPanelChange(true, this._attr.origin, this.container, true);
},
onCompleteScope: this
})
.to(this.panel, this._animatePanelDuration, { left: 0, ease: "easeOutCirc" })
.to(loadIndicator, this._animatePanelDuration, { right: this.panel.width() + 6, ease: "easeOutCirc" }, 0); // 6 is double border width
Theme.tooltipster(this.container);
this.update(this._attr);
},
/**
* Performs an update of the content and title of the SubPanel, running appropriate animation and `doOn-` / `doAfter-` functions.
*
* @method update
* @param {SubPanelSettings} a New settings for the SubPanel
*/
update: function (a) {
// helper functions
var animateContentDuration = 300,
that = this,
updateDefered = [new Deferred(), new Deferred()],
animateContent = function (node, newData, d) {
if (newData) {
node.addClass('animated fadeOutDown');
window.setTimeout(
function () {
node
//.html(newData)
.empty().append(newData)
.removeClass("fadeOutDown")
.addClass('animated fadeInDown'); //.find(".shorten-candidate").shorten();
d.resolve();
},
animateContentDuration);
}
},
setContent = function (node, oldData, newData, parsedData, visible, d) {
newData = (newData === null) ? parsedData = subPanelLoadingAnimation : newData;
if (newData) {
if (newData !== oldData) {
if (visible) {
//ideally, need to wait until the animation completes before proceeding?
animateContent(node, parsedData, d);
} else {
node.empty().append(parsedData);
d.resolve();
}
} else {
d.resolve();
}
} else {
d.resolve();
}
},
updateContent = function (a) {
// if the content in the subpanel is scrolled down, scroll back to the top
TweenLite.to(that._subPanelContentDiv, animateContentDuration / 1000,
{ scrollTop: 0, ease: "easeOutCirc" });
setContent(that._panelTitle, that._attr.title, a.title, a.title, that._visible, updateDefered[0]);
setContent(that._panelContentDiv, that._attr.content, a.content, that.parseContent(a.content), that._visible, updateDefered[1]);
dojoLang.mixin(that._attr, a);
};
// doAfterUpdate should be called AFTER update (animation) completes...
UtilMisc.afterAll(updateDefered, function () {
if (a.doAfterUpdate) {
a.doAfterUpdate();
}
});
// panel is closing; new data is not an update
if (this._closing && !a.update) {
//
if (this._attr.guid !== a.guid) {
if (this._attr.doOnHide) {
this._attr.doOnHide();
}
if (this._attr.doAfterHide) {
this._attr.doAfterHide();
}
// move panel to the new target
a.target.after(this.container);
updateContent(a);
}
this.reopen();
// panel is not closing
} else if (!this._closing) {
// data is not an update
if (!a.update && this._attr.guid !== a.guid) {
if (this._attr.doOnHide) {
this._attr.doOnHide();
}
if (this._attr.doAfterHide) {
this._attr.doAfterHide();
}
// move panel to the new target
a.target.after(this.container);
updateContent(a);
if (a.doOnOpen) {
a.doOnOpen();
}
if (a.doAfterOpen) {
a.doAfterOpen();
}
}
// guid is the same - data can update or not (should be an update)
//if (a.update && attr.guid === a.guid) {
if (this._attr.guid === a.guid) {
updateContent(a);
}
}
}
},
helpToggle = $("#helpToggle"),
helpSectionContainer = $("#help-section-container"),
helpSection = $("#help-section"),
cssButtonPressedClass = "button-pressed",
cssExpandedClass = "state-expanded",
helpPanelPopup,
transitionDuration = 0.5,
toolbarController,
layoutController;
toolbarController = (function () {
var mapToolbar,
mapTools,
toolbarWidth,
toolsWidth,
toolsTimeline = new TimelineLite(),
isTooltipsSet = false;
function updateToolbarStats() {
if (!layoutController.isFullData()) {
toolbarWidth = mapToolbar.width();
if (!isTooltipsSet) {
toolsWidth = mapTools
.map(function (i, n) { return $(n).outerWidth(); })
.get() // need to use get() as jquery.map doesn't return a true array
.reduce(function (total, width) { return total + width; })
;
}
if (!isToolbarWideEnough()) {
setToolbarTooltips();
} else {
removeToolbarTooltips();
}
}
}
function isToolbarWideEnough() {
return toolbarWidth > toolsWidth;
}
function setToolbarTooltips() {
toolsTimeline.invalidate();
if (!isTooltipsSet) {
// set tooltips on the collapsed toolbar
mapToolbar.addClass("compact");
mapTools
.filter(":not(.tooltipstered)")
.map(function (i, node) {
node = $(node);
node
.addClass("_tooltip tooltip-temp")
.attr(
"title",
node.find("span").text()
);
});
Theme.tooltipster(mapToolbar);
isTooltipsSet = true;
}
}
function removeToolbarTooltips() {
if (isTooltipsSet && isToolbarWideEnough()) {
// remove tooltips from the restored toolbar and only from the buttons with a temporary tooltip
Theme.tooltipster(
mapTools
.filter(".tooltip-temp")
.parent(),
null, "destroy");
mapToolbar
.removeClass("compact");
mapTools
.filter(".tooltip-temp")
.removeClass("_tooltip")
.removeAttr("title");
isTooltipsSet = false;
}
}
return {
init: function () {
mapToolbar = $("#map-toolbar");
mapTools = mapToolbar.find("> .map-toolbar-item > .map-toolbar-item-button");
updateToolbarStats();
jWindow.on("resize", updateToolbarStats);
return this;
},
mapTools: function () {
return mapTools;
},
toolsTimeline: function () {
return toolsTimeline;
},
update: function () {
updateToolbarStats();
return this;
},
setTooltips: function () {
setToolbarTooltips();
return this;
},
removeTooltips: function () {
removeToolbarTooltips();
return this;
}
};
}());
/**
* Controls layout transition such as full-data and full-screen modes, opening and closing of the side panel, adjusts layout when resizing the browser window.
*
* @class LayoutController
* @static
* @for GUI
*/
layoutController = (function () {
var viewport = $(".viewport"),
mapDiv = $("#map-div"),
mapContent = $("#mapContent"),
fullScreenToggle = $("#fullScreenToggle"),
fullScreenPopup,
mapToolbar = $("#map-toolbar"),
basemapControls = $("#basemapControls"),
panelDiv = $("#panel-div"),
panelToggle = $("#panel-toggle"),
panelPopup,
panelWidthDefault, // default width of the SidePanel.
layoutWidthThreshold = 1200, // minimum width of the wide layout
dataTabNode,
dataTabUpdateNotice,
windowWidth,
layoutChange,
_isFullData = false,
fullDataTimeLine = new TimelineLite({
paused: true,
onComplete: function () {
adjustHeight();
layoutChange();
toolbarController.setTooltips();
//console.log("finished", EventManager.Datagrid.APPLY_EXTENT_FILTER);
//topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
// kill key events to stop tab switching when the other tab is hidden
panelDiv.on("keydown", ".wb-tabs > ul > li > a", stopEventPropagation);
},
onReverseComplete: function () {
viewport.removeClass("full-data-mode");
adjustHeight();
layoutChange();
toolbarController
.update()
.removeTooltips();
//console.log("reverse finished", EventManager.Datagrid.APPLY_EXTENT_FILTER);
//topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
// restore key events
panelDiv.off("keydown", ".wb-tabs > ul > li > a", stopEventPropagation);
}
}),
panelToggleTimeLine = new TimelineLite({ paused: true }),
fullDataSubpanelChangeTimeLine = new TimelineLite({ paused: true }),
noDataTimeLine = new TimelineLite({
paused: true,
onComplete: function () {
// kill key events to stop tab switching when the other tab is hidden
panelDiv.on("keydown", ".wb-tabs > ul > li > a", stopEventPropagation);
},
onReverseComplete: function () {
// restore key events
panelDiv.off("keydown", ".wb-tabs > ul > li > a", stopEventPropagation);
}
}),
// timeline generating functions
createFullDataTL,
createPanelToggleTL,
createFullDataSubpanelChangeTL,
timeLines;
createFullDataTL = function () {
if (panelDiv.find(".wb-tabs > ul li").length === 0) {
return;
}
fullDataTimeLine
.fromTo(mapDiv, transitionDuration, { width: "auto" }, { width: 35, ease: "easeOutCirc" }, 0)
.fromTo(mapContent, transitionDuration, { opacity: 1 }, { opacity: 0, ease: "easeOutCirc" }, 0)
.set(mapContent, { top: "500px" })
.to(panelToggle, transitionDuration, { right: -13, ease: "easeOutCirc" }, 0)
.set(panelToggle, { display: "none" })
.to(basemapControls, transitionDuration / 2, { opacity: 0, ease: "easeOutCirc" }, 0)
.to(basemapControls, 0, { display: "none" }, transitionDuration / 2)
.fromTo(mapToolbar, transitionDuration / 2,
{ width: "100%", height: "32px" },
{ width: "32px", height: $("#map-div").height(), ease: "easeOutCirc" }, transitionDuration / 2)
.add(toolbarController.toolsTimeline(), 0)
//.fromTo(toolbarController.mapTools().find("> span"), transitionDuration / 2, { width: "auto" }, { width: 0, ease: "easeOutCirc" }, 0)
//.fromTo(toolbarController.mapTools().find("> span"), 0, { display: "inline-block" }, { display: "none" }, transitionDuration / 2)
.fromTo(panelDiv.find(".wb-tabs > ul li:first"), transitionDuration, { width: "50%" }, { width: "0%", display: "none", ease: "easeOutCirc" }, 0)
.fromTo(panelDiv.find(".wb-tabs > ul li:last"), transitionDuration, { width: "50%" }, { width: "100%", className: "+=h5", ease: "easeOutCirc" }, 0)
.fromTo(panelDiv, transitionDuration,
{ width: panelWidthDefault, left: "auto" },
{ left: 35, width: "auto", ease: "easeOutCirc" }, 0);
};
createPanelToggleTL = function () {
panelToggleTimeLine
.fromTo(panelDiv, transitionDuration, { right: 0 }, { right: -panelWidthDefault, ease: "easeOutCirc" }, 0)
.set(panelDiv, { display: "none" }, transitionDuration)
.fromTo(mapDiv, transitionDuration, { right: panelWidthDefault }, { right: 0, ease: "easeOutCirc" }, 0);
};
createFullDataSubpanelChangeTL = function () {
fullDataSubpanelChangeTimeLine
.fromTo(panelDiv, transitionDuration,
{ right: 0 },
{ right: panelWidthDefault, ease: "easeOutCirc" });
};
timeLines = [
{
timeLine: fullDataTimeLine,
generator: createFullDataTL
},
{
timeLine: panelToggleTimeLine,
generator: createPanelToggleTL
},
{
timeLine: fullDataSubpanelChangeTimeLine,
generator: createFullDataSubpanelChangeTL
}
];
/**
* Fires an event when the layout of the page changes.
*
* @method layoutChange
* @private
* @for LayoutController
*/
layoutChange = function () {
if (!_isFullData) {
console.log("GUI --> EventManager.GUI.LAYOUT_CHANGE");
topic.publish(EventManager.GUI.LAYOUT_CHANGE);
}
};
/**
* Adjusts the height of the help section based on the height of the window.
*
* @method adjustHeight
* @private
*/
function adjustHeight() {
helpSection.css({
"max-height": jWindow.height() - (_isFullData ? jWindow.height() * 0.2 : mapToolbar.offset().top) - 90 // 90 is an arbitrary-wide gap between the help panel and the upper toolbar
});
}
/**
* Executed after full-screen mode transition is complete.
*
* @method onFullScreenComplete
* @private
*/
function onFullScreenComplete() {
adjustHeight();
layoutChange();
topic.publish(EventManager.GUI.FULLSCREEN_CHANGE, {
visible: Theme.isFullScreen()
});
}
/**
* Publishes `PANEL_CHANGE` event when the visibility of the SidePanel changes.
*
* @method panelChange
* @param {Boolean} visible Indicates whether the SidePanel is visible or not
* @private
*/
function panelChange(visible) {
topic.publish(EventManager.GUI.PANEL_CHANGE, {
visible: visible
});
}
/**
* Slides the SidePanel open.
*
* @method openPanel
* @private
* @param {Deferred} d A deferred to be resolved upon completion of the animation
*/
function openPanel(d) {
/*jshint validthis: true */
panelToggleTimeLine.eventCallback("onReverseComplete",
function () {
layoutChange();
panelChange(true);
toolbarController.update();
// update close button tooltips
panelToggle
.tooltipster("content", i18n.t("gui.actions.close"))
.find("span.wb-invisible").text(i18n.t("gui.actions.close"));
d.resolve();
}, [], this);
viewport.removeClass("no-sidepanel-mode");
panelToggleTimeLine.reverse();
}
/**
* Slide the SidePanel close
*
* @method closePanel
* @private
* @param {Deferred} d A deferred to be resolved upon completion of the animation
*/
function closePanel(d) {
/*jshint validthis: true */
panelToggleTimeLine.eventCallback("onComplete",
function () {
console.log("GUI <-- map/update-end from gui");
layoutChange();
panelChange(false);
toolbarController.update();
// update open button tooltips
panelToggle
.tooltipster("content", i18n.t("gui.actions.open"))
.find("span.wb-invisible").text(i18n.t("gui.actions.open"));
viewport.addClass("no-sidepanel-mode");
d.resolve();
}, [], this);
panelToggleTimeLine.play();
}
/**
* Toggles the full-data mode on and off.
*
* @method _toggleFullDataMode
* @param {Boolean} [fullData] if true, switches to full-data mode; if false, switches to summary mode; if undefined, toggle mode based on the current state
* @private
*/
function _toggleFullDataMode(fullData) {
_isFullData = typeof fullData === 'boolean' ? fullData : !_isFullData;
// if the timeline duration is 0, reset it
// it's to work-around IE bug where it's so slow, it can't pick up nodes created by WET scripts when creating timelines
if (fullDataTimeLine.totalDuration() === 0) {
UtilMisc.resetTimelines([timeLines[0]]);
}
if (_isFullData) {
viewport.addClass("full-data-mode"); // set full-data-mode css class BEFORE animation; remove it after it finishes - on onReverseComplete callback
fullDataTimeLine.play();
} else {
fullDataSubpanelChangeTimeLine.reverse(); // play this animation to readjust the sidepanel if the details panel was opened and not closed in full data mode
fullDataTimeLine.reverse();
}
// close subpanels
utilDict.forEachEntry(subPanels, function (key) {
hideSubPanel({
origin: key
});
});
}
/**
* Changes internal panel width reference based on the window width.
*
* @method updatePanelWidth
* @private
*/
function updatePanelWidth() {
panelWidthDefault = windowWidth < layoutWidthThreshold ? 340 : 430;
}
/**
* Optimizes layout based on the window width
*
* @method optimizeLayout
* @private
*/
function optimizeLayout() {
if ((windowWidth < layoutWidthThreshold && jWindow.width() > layoutWidthThreshold) ||
(windowWidth > layoutWidthThreshold && jWindow.width() < layoutWidthThreshold)) {
windowWidth = jWindow.width();
updatePanelWidth();
UtilMisc.resetTimelines(timeLines, true);
}
}
/**
* Setup noDataTimeLine for data tab animation
*
* @method setupNoDataTimeLine
* @private
*/
function setupNoDataTimeLine() {
// timeline that hides Data tab
noDataTimeLine
.fromTo(panelDiv.find(".wb-tabs > ul li:first"), transitionDuration, { width: "50%" }, { width: "100%", className: "+=h5", ease: "easeOutCirc" }, 0)
.to(panelDiv.find(".wb-tabs > ul li:first"), transitionDuration, { lineHeight: '20px' }, 0)
.fromTo(panelDiv.find(".wb-tabs > ul li:last"), transitionDuration, { width: "50%" }, { width: "0%", display: "none", ease: "easeOutCirc" }, 0)
;
}
return {
/**
* Initializes layout controller.
*
* @method init
*/
init: function () {
windowWidth = jWindow.width();
jWindow.on("resize", optimizeLayout);
updatePanelWidth();
toolbarController.init();
UtilMisc.resetTimelines(timeLines);
Theme
.fullScreenCallback("onComplete", onFullScreenComplete)
.fullScreenCallback("onReverseComplete", onFullScreenComplete);
// initialize the panel popup
panelPopup = popupManager.registerPopup(panelToggle, "click",
openPanel, {
activeClass: cssExpandedClass,
closeHandler: closePanel
}
);
// set listener to the panel toggle
topic.subscribe(EventManager.GUI.PANEL_TOGGLE, function (event) {
panelPopup.toggle(null, event.visible);
});
// set listener to the full-screen toggle
fullScreenPopup = popupManager.registerPopup(fullScreenToggle, "click",
function (d) {
Theme.toggleFullScreenMode();
d.resolve();
}, {
activeClass: "button-pressed",
setClassBefore: true
}
);
// if the vertical space is too small, trigger the full-screen
if (mapContent.height() < jWindow.height() * 0.6) {
fullScreenPopup.open();
}
// find a Data tab append an update notice to it
dataTabUpdateNotice = $(TmplHelper.template('datagrid_notice_update', {}, datagridTemplate));
dataTabNode = panelDiv.find(".wb-tabs > ul li:last");
dataTabNode.append(dataTabUpdateNotice);
topic.subscribe(EventManager.Datagrid.UPDATING, function (event) {
dataTabUpdateNotice.toggle(event);
});
adjustHeight();
// setup noDataTimeLine here to allow IE9 to have enough time to
// create the tab
setupNoDataTimeLine();
},
/**
* Toggles the FullScreen mode of the application
*
* @method toggleFullScreenMode
* @param {Boolean} fullscreen true - expand; false - collapse; undefined - toggle;
*/
toggleFullScreenMode: function (fullscreen) {
fullScreenPopup.toggle(null, fullscreen);
},
/**
* Toggles the visibility of Data tab in the side panel
*
* @method toggleDataTab
* @param {Boolean} open true - show; false - hide;
*/
toggleDataTab: function (open) {
if (open) {
noDataTimeLine.reverse();
} else {
noDataTimeLine.play();
}
},
/**
* Returns the state of the full-data mode.
*
* @method isFullData
* @return {Boolean} True is full-data mode on; false otherwise
*/
isFullData: function () {
return _isFullData;
},
/**
* Toggles the full-data mode on and off.
*
* @method toggleFullDataMode
* @param {Boolean} [fullData] if true, switches to full-data mode; if false, switches to summary mode; if undefined, toggle mode based on the current state
*/
toggleFullDataMode: function (fullData) {
_toggleFullDataMode(fullData);
},
/**
* Fires an event when the subpanel closes or opens.
*
* @method subPanelChange
* @param {Boolean} visible indicates whether the subpanel is visible or not (the panel is considered invisible when it's being destroyed, starts closing)
* @param {String} origin origin of the subpanel
* @param {JObject} container subpanel container
* @param {Boolean} isComplete indicates if subPanel transition has completed or just started
*/
subPanelChange: function (visible, origin, container, isComplete) {
// check if the fullData transition is already underway
if (!fullDataTimeLine.isActive() && _isFullData && !isComplete) {
// adjust the sidePanel position shifting the right edge to the left, making space for the subpanel to open at
if (visible) {
fullDataSubpanelChangeTimeLine.play();
} else if (!visible) {
fullDataSubpanelChangeTimeLine.reverse();
}
}
// hide the sidepanel toggle when a subpanel is opened to prevent the user from closing sidepanel with subpanel still opened
if (!isComplete) {
if (visible) {
panelToggle.hide();
} else {
panelToggle.show();
}
}
topic.publish(EventManager.GUI.SUBPANEL_CHANGE, {
visible: visible,
origin: origin,
container: container,
offsetLeft: (container) ? container.width() + 25 + layoutController.getPanelWidth() : layoutController.getPanelWidth(),
isComplete: isComplete
});
},
/**
* Returns the outer most `div` of this SidePanel.
*
* @method getContainer
* @return {jObject} The outer most `div` of this SidePanel
*/
getPanelContainer: function () {
return panelDiv;
},
/**
* Gets the width of this SidePanel.
*
* @method width
* @return {Number} The width of this SidePanel
* @for LayoutController
*/
getPanelWidth: function () {
return panelDiv.filter(":visible").width();
}
};
}());
/**
* Create a new SubPanel with the settings provided.
*
* @private
* @method newSubPanel
* @param {SubPanelSettings} attr SubPanel settings
* @return {SubPanel} A newly created SubPanel
* @for GUI
*/
function newSubPanel(attr) {
var subPanel = Object.create(subPanelPrototype);
subPanel._attr = Object.create(subPanelAttr);
subPanel.create(attr);
//adjutSubPanelDimensions(subPanel);
return subPanel;
}
/**
* Creates and opens a new SubPanel with given settings.
* If the SubPanel with the requested `origin` is already present, updates its content.
*
* @method showSubPanel
* @private
* @param {SubPanelSettings} attr Settings for the SubPanel instance
*/
function showSubPanel(attr) {
var deferred = new Deferred(),
subPanel;
deferred.then(function () {
attr = subPanel.getAttributes();
subPanel = subPanels[attr.origin];
subPanel.open();
subPanel.getPanel().find(".sub-panel-toggle")
.on("click", function () {
hideSubPanel(attr);
// reset focus back to link where the subpanel was created from
if (attr.target.selector !== "#map-div") {
$(attr.target).find(":tabbable").first().focus();
}
});
});
// take over the panel spawned by other components
if (attr.consumeOrigin && subPanels[attr.consumeOrigin]) {
subPanel = subPanels[attr.consumeOrigin];
subPanel.changeOrigin(attr.origin);
subPanel.shiftTarget(attr.target);
delete subPanels[attr.consumeOrigin];
subPanels[attr.origin] = subPanel;
}
if (subPanels[attr.origin]) {
// if the panel exists, just update it
subPanels[attr.origin].update(attr);
} else if (!attr.update) {
// create if doesn't
subPanel = newSubPanel(attr);
subPanels[attr.origin] = subPanel;
// close all other panels; and open the newly created one after all others are closed
UtilMisc.executeOnDone(subPanels,
function (p, d) {
if (p && p.getOrigin() !== attr.origin) {
hideSubPanel({
origin: p.getOrigin()
}, 200, d);
} else {
d.resolve(true);
}
},
deferred);
}
}
/**
* Closes the SubPanel whose `origin` is specified in the `attr` parameter.
*
* @method hideSubPanel
* @private
* @param {SubPanelSettings} attr only `origin` attribute is required here
* @param {Number} speed Duration of the closing animation
* @param {Deferred} d The deferred object to be resolved upon successful closing of the panel
*/
function hideSubPanel(attr, speed, d) {
var deferred = new Deferred(function () {
if (d) {
d.cancel();
}
});
deferred.then(function () {
// remove the panel from the object after it closes
delete subPanels[attr.origin]; // more on delete: http://perfectionkills.com/understanding-delete/
if (d) {
d.resolve(true);
}
});
if (subPanels[attr.origin]) {
subPanels[attr.origin].destroy(speed, deferred);
}
}
/**
* Moves the SubPanel with the specified `origin` in the DOM hierarchy to the new specified `target`; if `target` is not specified, the SubPanel is attached to the SidePanel.
*
* @method dockSubPanel
* @private
* @param {SubPanelSettings} attr Settings for the SubPanel; only `target` and `origin` are required here
*/
function dockSubPanel(attr) {
var target = attr.target || layoutController.getPanelContainer(),
subPanel = subPanels[attr.origin];
if (subPanel) {
//console.log("docking subpanel");
subPanel.shiftTarget(target);
}
}
/**
* Finds a SubPanel with `origin` equal to the supplied `consumeOrigin` and
* + changes its `origin` to the supplied `origin`
* + moves the SubPanel in the DOM hierarchy and attaches it to the specified target
*
* @method captureSubPanel
* @private
* @param {SubPanelSettings} attr Settings for the SubPanel; only `origin`, `consumeOrigin` and `target` are required here
*/
function captureSubPanel(attr) {
var subPanel;
if (attr.consumeOrigin === attr.origin && subPanels[attr.consumeOrigin]) {
subPanel = subPanels[attr.origin];
subPanel.shiftTarget(attr.target);
} else if (attr.consumeOrigin && subPanels[attr.consumeOrigin]) {
subPanel = subPanels[attr.consumeOrigin];
subPanel.changeOrigin(attr.origin);
subPanel.shiftTarget(attr.target);
delete subPanels[attr.consumeOrigin];
subPanels[attr.origin] = subPanel;
}
}
// TODO: notify bookmark module that layer query toggle has changed its state
/**
* Stops event propagation.
*
* @method stopEventPropagation
* @private
*/
function stopEventPropagation(event) {
event.stopPropagation();
}
/**
* A helper function that shows/hides Data tab if there some/none feature layers in the layer selector.
*
* @method autoHideDataTab
* @private
*/
function autoHideDataTab() {
// initialize to the correct state (this might be happening after some layers have already loaded)
if (RAMP.layerCounts && RAMP.layerCounts.feature === 0) {
layoutController.toggleDataTab();
}
// subscribe to Layer added event which is fired every time a layer is added to the map through layer loader
topic.subscribe(EventManager.LayerLoader.LAYER_ADDED, function (args) {
layoutController.toggleDataTab(args.layerCounts.feature > 0);
});
// on each remove check if there are still feature layers in the layer list
topic.subscribe(EventManager.LayerLoader.LAYER_REMOVED, function (args) {
layoutController.toggleDataTab(args.layerCounts.feature > 0);
});
}
return {
/**
* Call load to initialize the GUI module.
*
* @method load
* @param {Number} id ID of this module
* @param {Object} req dojo required, can be used to require additional modules, etc.
* @param {Function} load The callback function to be called as the very last thing in load
*/
load: function (id, req, load) {
// measure available space on every page resize
subPanelTemplate = JSON.parse(TmplHelper.stringifyTemplate(subPanelTemplate));
subPanelLoadingAnimation = TmplHelper.template('loading_simple', null, subPanelTemplate);
datagridTemplate = JSON.parse(TmplHelper.stringifyTemplate(datagridTemplate));
layoutController.init();
// registering help popup
helpPanelPopup = popupManager.registerPopup(helpToggle, "click",
function (d) {
topic.publish(EventManager.GUI.HELP_PANEL_CHANGE, { visible: true });
topic.publish(EventManager.GUI.TOOLBAR_SECTION_OPEN, { id: "help-section" });
console.log(EventManager.GUI.HELP_PANEL_CHANGE + "; visible:", true);
var that = this;
// close this panel if any other panel is opened
UtilMisc.subscribeOnce(EventManager.GUI.TOOLBAR_SECTION_OPEN,
function () {
if (that.isOpen()) {
that.close();
}
});
helpSectionContainer.slideToggle("fast", function () {
d.resolve();
});
}, {
activeClass: cssButtonPressedClass,
target: helpSectionContainer,
closeHandler: function (d) {
topic.publish(EventManager.GUI.HELP_PANEL_CHANGE, { visible: false });
topic.publish(EventManager.GUI.TOOLBAR_SECTION_CLOSE, { id: "help-section" });
console.log(EventManager.GUI.HELP_PANEL_CHANGE + "; visible:", false);
helpSectionContainer.slideToggle("fast", function () {
d.resolve();
});
},
resetFocusOnClose: true
}
);
// TODO: remove mapQueryToggle.autoHide from config schema
autoHideDataTab();
//start extended grid
topic.subscribe(EventManager.GUI.DATAGRID_EXPAND, function () {
layoutController.toggleFullDataMode();
});
//end extended grid
topic.subscribe(EventManager.GUI.TOGGLE_FULLSCREEN, function (evt) {
layoutController.toggleFullScreenMode(evt.expand);
});
topic.subscribe(EventManager.GUI.SUBPANEL_OPEN, function (attr) {
showSubPanel(attr);
});
topic.subscribe(EventManager.GUI.SUBPANEL_CLOSE, function (attr) {
if (attr.origin === "all") {
utilDict.forEachEntry(subPanels, function (key) {
//attr.origin = key;
hideSubPanel({
origin: key
});
});
} else {
attr.origin.split(",").forEach(function (element) {
//attr.origin = element;
hideSubPanel({
origin: element
});
});
}
});
topic.subscribe(EventManager.GUI.SUBPANEL_DOCK, function (attr) {
var na;
if (attr.origin === "all") {
utilDict.forEachEntry(subPanels, function (key) {
na = Object.create(attr);
na.origin = key;
dockSubPanel(na);
});
} else {
attr.origin.split(",").forEach(function (element) {
na = Object.create(attr);
na.origin = element;
dockSubPanel(na);
});
}
console.log(EventManager.GUI.SUBPANEL_DOCK, attr);
});
topic.subscribe(EventManager.GUI.SUBPANEL_CAPTURE, function (attr) {
var na;
if (attr.consumeOrigin === "all") {
utilDict.forEachEntry(subPanels, function (key) {
na = Object.create(attr);
na.consumeOrigin = key;
captureSubPanel(na);
});
} else {
attr.consumeOrigin.split(",").forEach(function (element) {
na = Object.create(attr);
na.consumeOrigin = element;
captureSubPanel(na);
});
}
});
// since basemap toggle may differ in width based on the basemap name, check if everything still fits
topic.subscribe(EventManager.BasemapSelector.UI_COMPLETE, function () {
toolbarController.update();
});
topic.subscribe(EventManager.BasemapSelector.BASEMAP_CHANGED, function () {
toolbarController.update();
});
sidePanelTabList.find("li a").click(function () {
console.log("inside side panel tab list on click");
var selectedPanelId = $(this).attr("href").substr(1);
sidePanelTabPanels.find("details[id=" + selectedPanelId + "]").each(
function () {
topic.publish(EventManager.GUI.TAB_SELECTED, {
id: this.id,
tabName: $(this).data("panel-name")
});
});
// the panel currently open is being deselected
sidePanelTabPanels.find("details[aria-expanded=true]").each(
function () {
topic.publish(EventManager.GUI.TAB_DESELECTED, {
id: this.id,
tabName: $(this).data("panel-name")
});
});
});
// List of objects containing an event name and an event argument. The events should
// be anything to wait for before publishing a map extent change, it should include
// anything that will change the size of the map (e.g. fullscreen, closing the panel).
// If the map extent change occurs BEFORE something that changes the size of the map (e.g. fullscreen)
// then the map extent will change again.
var waitList = [];
if (!RAMP.state.ui.sidePanelOpened) {
// NOTE: panel change not triggered here (see map extent change below)
waitList.push({
publishName: EventManager.GUI.PANEL_TOGGLE,
eventArg: {
origin: "bootstrapper",
visible: RAMP.state.ui.sidePanelOpened
},
subscribeName: EventManager.GUI.PANEL_CHANGE
});
}
if (RAMP.state.ui.fullscreen) {
// NOTE: fullscreen not triggered here (see map extent change below)
waitList.push({
publishName: EventManager.GUI.TOGGLE_FULLSCREEN,
eventArg: {
expand: true
},
subscribeName: EventManager.GUI.FULLSCREEN_CHANGE
});
}
// return the callback
load();
// This should be the last thing that happens
if (waitList.isEmpty()) {
topic.publish(EventManager.GUI.UPDATE_COMPLETE);
} else {
// Wait for things such as fullscreen or panel collapse
// to finish before publishing the UPDATE_COMPLETE.
// Note it's important to subscribe to the events, then
// publish them, that's why it was done in such an obscure way
// using the waitList (otherwise if we just publish the
// event like above, then subscribe to it here, the event
// might have completed before reaching this point)
var eventNames = waitList.map(function (obj) {
return obj.subscribeName;
});
UtilMisc.subscribeAll(eventNames, function () {
topic.publish(EventManager.GUI.UPDATE_COMPLETE);
});
waitList.forEach(function (obj) {
topic.publish(obj.publishName, obj.eventArg);
});
}
}
};
}
);