/* * View framework for JamKazam. * * Processes proprietary attributes in markup to convert a set of HTML elements * into the JamKazam screen layout. This module is only responsible for size * and position. All other visual aspects should be elsewhere. * * See the layout-example.html file for a simple working example. */ (function(context,$) { "use strict"; context.JK = context.JK || {}; // Static function to hide the 'curtain' which hides the underlying // stuff until we can get it laid out. Called from both the main // client as well as the landing page. context.JK.hideCurtain = function(duration) { context.setTimeout(function() { $('.curtain').fadeOut(2*duration); }, duration); }; context.JK.Layout = function() { // privates var logger = context.JK.logger; var me = null; // Reference to this instance for context sanity. var opts = { headerHeight: 75, sidebarWidth: 300, notifyHeight: 150, notifyGutter: 10, collapsedSidebar: 30, panelHeaderHeight: 36, gutter: 60, // Margin around the whole UI screenMargin: 0, // Margin around screens (not headers/sidebar) gridOuterMargin: 6, // Outer margin on Grids (added to screenMargin if screen) gridPadding: 8, // Padding around grid cells. Added to outer margin. animationDuration: 400, allowBodyOverflow: false // Allow tests to disable the body-no-scroll policy }; var width = $(context).width(); var height = $(context).height(); var resizing = null; var sidebarVisible = true; var expandedPanel = null; var previousScreen = null; var currentScreen = null; var screenBindings = {}; var dialogBindings = {}; var wizardShowFunctions = {}; var openDialogs = []; // FIFO stack function setup() { requiredStyles(); hideAll(); setInitialExpandedSidebarPanel(); sizeScreens(width, height, '[layout="screen"]', true); positionOffscreens(width, height); $('[layout="sidebar"]').show(); $('[layout="panel"]').show(); layout(); } function setInitialExpandedSidebarPanel() { expandedPanel = $('[layout="panel"]').first().attr("layout-id"); } function layout() { width = $(context).width(); height = $(context).height(); // TODO // Work on naming. File is layout, class is Layout, this method // is layout and every other method starts with 'layoutX'. Perhaps // a little redundant? layoutCurtain(width, height); layoutDialogOverlay(width, height); layoutScreens(width, height); layoutSidebar(width, height); layoutHeader(width, height); layoutNotify(width, height); layoutFooter(width, height); } function layoutCurtain(screenWidth, screenHeight) { var curtainStyle = { position: 'absolute', width: screenWidth + 'px', height: screenHeight + 'px' }; $('.curtain').css(curtainStyle); } function layoutDialogOverlay(screenWidth, screenHeight) { var style = { position: 'absolute', width: screenWidth + 'px', height: screenHeight + 'px' }; $('.dialog-overlay').css(style); } function layoutScreens(screenWidth, screenHeight) { var previousScreenSelector = '[layout-id="' + previousScreen + '"]'; var currentScreenSelector = '[layout-id="' + currentScreen + '"]'; $(currentScreenSelector).show(); var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); var left = -1 * width - 100; if (currentScreenSelector === previousScreenSelector) { left = $(currentScreenSelector).css("left"); if (left) { left = left.split("px")[0]; } } $(previousScreenSelector).animate({left: left}, {duration: opts.animationDuration, queue: false}); sizeScreens(screenWidth, screenHeight, '[layout="screen"]'); positionOffscreens(screenWidth, screenHeight); positionOnscreen(screenWidth, screenHeight); } function sizeScreens(screenWidth, screenHeight, selector, immediate) { var duration = opts.animationDuration; if (immediate) { duration = 0; } var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); if (sidebarVisible) { width -= (opts.sidebarWidth + 2*opts.gridPadding); } else { width -= opts.collapsedSidebar + 2*opts.gridPadding; width += opts.gutter; // Add back in the right gutter width. } var height = screenHeight - opts.headerHeight - (2 * opts.gutter + 2 * opts.screenMargin); var css = { width: width, height: height }; var $screens = $(selector); $screens.animate(css, {duration:duration, queue:false}); layoutHomeScreen(width, height); } /** * Postition all screens that are not the current screen. */ function positionOffscreens(screenWidth, screenHeight) { var top = opts.headerHeight + opts.gutter + opts.screenMargin; var left = -1 * (screenWidth + 2*opts.gutter); var $screens = $('[layout="screen"]').not('[layout-id="' + currentScreen + '"]'); $screens.css({ top: top, left: left }); } /** * Position the current screen */ function positionOnscreen(screenWidth, screenHeight, immediate) { var duration = opts.animationDuration; if (immediate) { duration = 0; } var top = opts.headerHeight + opts.gutter + opts.screenMargin; var left = opts.gutter + opts.screenMargin; var $screen = $('[layout-id="' + currentScreen + '"]'); $screen.animate({ top: top, left: left, overflow: 'auto' }, duration); } function layoutHomeScreen(homeScreenWidth, homeScreenHeight) { var $grid = $('[layout-grid]'); var gridWidth = homeScreenWidth; var gridHeight = homeScreenHeight; $grid.css({width:gridWidth, height: gridHeight}); var layout = $grid.attr('layout-grid'); if (!layout) return; var gridRows = layout.split('x')[0]; var gridCols = layout.split('x')[1]; $grid.children().each(function() { var childPosition = $(this).attr("layout-grid-position"); var childRow = childPosition.split(',')[1]; var childCol = childPosition.split(',')[0]; var childRowspan = $(this).attr("layout-grid-rows"); var childColspan = $(this).attr("layout-grid-columns"); var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols, childRow, childCol, childRowspan, childColspan); $(this).animate({ width: childLayout.width, height: childLayout.height, top: childLayout.top, left: childLayout.left }, opts.animationDuration); }); } function layoutSidebar(screenWidth, screenHeight) { var width = opts.sidebarWidth; var expanderHeight = $('[layout-sidebar-expander]').height(); var height = screenHeight - opts.headerHeight - 2 * opts.gutter + expanderHeight; var right = opts.gutter; if (!sidebarVisible) { // Negative right to hide most of sidebar right = (0 - opts.sidebarWidth) + opts.collapsedSidebar; } var top = opts.headerHeight + opts.gutter - expanderHeight; var css = { width: width, height: height, top: top, right: right }; $('[layout="sidebar"]').animate(css, opts.animationDuration); layoutPanels(width, height); if (sidebarVisible) { $('[layout-panel="collapsed"]').hide(); $('[layout-panel="expanded"]').show(); $('[layout-sidebar-expander="hidden"]').hide(); $('[layout-sidebar-expander="visible"]').show(); } else { $('[layout-panel="collapsed"]').show(); $('[layout-panel="expanded"]').hide(); $('[layout-sidebar-expander="hidden"]').show(); $('[layout-sidebar-expander="visible"]').hide(); } } function layoutPanels(sidebarWidth, sidebarHeight) { // TODO - don't like the accordian - poor usability. Requires longest mouse // reach when switching panels. Probably better to do tabs. if (!sidebarVisible) { return; } var $expandedPanelContents = $('[layout-id="' + expandedPanel + '"] [layout-panel="contents"]'); var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight; var searchHeight = $('.sidebar .search').first().height(); var expanderHeight = $('[layout-sidebar-expander]').height(); var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight + searchHeight); $('[layout-panel="contents"]').hide(); $('[layout-panel="contents"]').css({"height": "1px"}); $expandedPanelContents.show(); $expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration); } function layoutHeader(screenWidth, screenHeight) { var width = screenWidth - 2*opts.gutter; var height = opts.headerHeight - opts.gutter; var top = opts.gutter; var left = opts.gutter; var css = { width: width + "px", height: height + "px", top: top + "px", left: left + "px" }; $('[layout="header"]').css(css); } function layoutNotify(screenWidth, screenHeight) { var $notify = $('[layout="notify"]'); var nHeight = $notify.height(); var notifyStyle = { bottom: '0px', position: 'fixed' }; $notify.css(notifyStyle); } function layoutFooter(screenWidth, screenHeight) { if(!opts.layoutFooter) { return; } var $footer = $('#footer'); $footer.show(); var nHeight = $footer.height(); var footerStyle = { top: (screenHeight - 80) + 'px' }; var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); var left = -1 * width - 100; $footer.animate({ "left" : opts.gutter, "width" : width, "top": (screenHeight - 78) + "px"}, opts.animationDuration); } function requiredLayoutStyles() { var layoutStyle = { position: 'absolute', margin: '0px', padding: '0px' }; $('[layout]').css(layoutStyle); // JW: Setting z-index of notify to 1001, so it will appear above the dialog overlay. // This allows dialogs to use the notification. $('[layout="notify"]').css({"z-index": "1001", "padding": "20px"}); $('[layout="panel"]').css({position: 'relative'}); $('[layout-panel="expanded"] [layout-panel="header"]').css({ margin: "0px", padding: "0px", height: opts.panelHeaderHeight + "px" }); $('[layout-grid]').css({ position: "relative" }); $('[layout-grid]').children().css({ position: "absolute" }); } function requiredStyles() { var bodyStyle = { margin: '0px', padding: '0px', overflow: 'hidden' }; if (opts.allowBodyOverflow) { delete bodyStyle.overflow; } $('body').css(bodyStyle); requiredLayoutStyles(); var curtainStyle = { position: "absolute", margin: '0px', padding: '0px', overflow: 'hidden', zIndex: 100 }; $('.curtain').css(curtainStyle); } function hideAll() { $('[layout]').hide(); $('[layout="header"]').show(); } function showSidebar() { sidebarVisible = true; layout(); } function hideSidebar() { sidebarVisible = false; layout(); } function toggleSidebar() { if (sidebarVisible) { hideSidebar(); } else { showSidebar(); } } function hideDialogs() { // TODO - may need dialogEvents here for specific dialogs. $('[layout="dialog"]').hide(); $('.dialog-overlay').hide(); } function tabClicked(evt) { evt.preventDefault(); var destination = $(evt.currentTarget).attr('tab-target'); $('[tab-target]').removeClass('selected'); $(evt.currentTarget).addClass('selected'); $('.tab').hide(); $('[tab-id="' + destination + '"]').show(); } function linkClicked(evt) { evt.preventDefault(); var $currentTarget = $(evt.currentTarget); // allow links to be disabled if($currentTarget.hasClass("disabled") ) { return; } // If link requires FTUE, show that first. if ($currentTarget.hasClass("requires-ftue")) { if (!(context.jamClient.FTUEGetStatus())) { app.layout.showDialog('ftue'); } } var destination = $(evt.currentTarget).attr('layout-link'); var destinationType = $('[layout-id="' + destination + '"]').attr("layout"); if (destinationType === "screen") { context.location = '#/' + destination; } else if (destinationType === "dialog") { showDialog(destination); } } function close(evt) { var $target = $(evt.currentTarget).closest('[layout]'); var layoutId = $target.attr('layout-id'); var isDialog = ($target.attr('layout') === 'dialog'); if (isDialog) { closeDialog(layoutId); } else { $target.hide(); } return false; } function closeDialog(dialog) { console.log("closing dialog: " + dialog); var $dialog = $('[layout-id="' + dialog + '"]'); dialogEvent(dialog, 'beforeHide'); var $overlay = $('.dialog-overlay'); unstackDialogs($overlay); $dialog.hide(); dialogEvent(dialog, 'afterHide'); } function isScreenName(screenName) { if(!screenName) return false; var hashIndex = screenName.indexOf('#'); if(hashIndex > -1) { screenName = screenName.substr(hashIndex); } return screenBindings[screenName]; } function screenEvent(screen, evtName, data) { if (screen && screen in screenBindings) { if (evtName in screenBindings[screen]) { screenBindings[screen][evtName].call(me, data); } } } function dialogEvent(dialog, evtName, data) { if (dialog && dialog in dialogBindings) { if (evtName in dialogBindings[dialog]) { var result = dialogBindings[dialog][evtName].call(me, data); if(result === false) { return false; } } } return true; } function changeToScreen(screen, data) { previousScreen = currentScreen; currentScreen = screen; screenEvent(previousScreen, 'beforeHide', data); screenEvent(currentScreen, 'beforeShow', data); // For now -- it seems we want it open always. // TODO - support user preference here? Remember how they left it? sidebarVisible = true; /* var openSidebarScreens = [ 'home', 'session', 'createSession', 'findSession', 'searchResults' ]; $.each(openSidebarScreens, function() { logger.debug("comparing " + this + " to " + currentScreen); if (this === currentScreen) { sidebarVisible = true; return false; } }); */ layout(); screenEvent(previousScreen, 'afterHide', data); screenEvent(currentScreen, 'afterShow', data); // Show any requested dialog if ("d" in data) { showDialog(data.d); } } /** * Responsible for keeping N dialogs in correct stacked order, * also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one */ function stackDialogs($dialog, $overlay) { // don't push a dialog on the stack that is already on there; remove it from where ever it is currently // and the rest of the code will make it end up at the top var layoutId = $dialog.attr('layout-id'); for(var i = openDialogs.length - 1; i >= 0; i--) { if(openDialogs[i].attr('layout-id') === layoutId) { openDialogs.splice(i, 1); } } openDialogs.push($dialog); var zIndex = 1000; for(var i in openDialogs) { var $openDialog = openDialogs[i]; $openDialog.css('zIndex', zIndex); zIndex++; } $overlay.css('zIndex', zIndex - 1); } function unstackDialogs($overlay) { if(openDialogs.length > 0) { openDialogs.pop(); } var zIndex = 1000 + openDialogs.length; $overlay.css('zIndex', zIndex - 1); if(openDialogs.length == 0) { $overlay.hide(); } } function showDialog(dialog) { if(!dialogEvent(dialog, 'beforeShow')) {return;} var $overlay = $('.dialog-overlay') $overlay.show(); centerDialog(dialog); var $dialog = $('[layout-id="' + dialog + '"]'); stackDialogs($dialog, $overlay); $dialog.show(); dialogEvent(dialog, 'afterShow'); } function centerDialog(dialog) { var $dialog = $('[layout-id="' + dialog + '"]'); $dialog.css({ left: width/2 - ($dialog.width()/2) + "px", top: height/2 - ($dialog.height()/2) + "px" }); } function panelHeaderClicked(evt) { evt.preventDefault(); expandedPanel = $(evt.currentTarget).closest('[layout="panel"]').attr("layout-id"); layout(); return false; } function wizardLinkClicked(evt) { evt.preventDefault(); var targetStepId = $(evt.currentTarget).attr("layout-wizard-link"); setWizardStep(targetStepId); return false; } function setWizardStep(targetStepId) { var selector = '[layout-wizard-step="' + targetStepId + '"]'; var $targetStep = $(selector); var stepDialogTitle = $targetStep.attr("dialog-title"); if (stepDialogTitle) { var $myDialog = $targetStep.closest('[layout="dialog"]'); var $myTitle = $('.content-head h1', $myDialog); $myTitle.html(stepDialogTitle); } // Hide all steps: // Invoke the 'show' function, if present prior to actually showing. if (context._.contains(context._.keys(wizardShowFunctions), targetStepId)) { wizardShowFunctions[targetStepId](); } $('[layout-wizard-step]').hide(); $targetStep.show(); var ftuePurpose = $targetStep.attr("dialog-purpose"); context.JK.GA.trackFTUECompletion(ftuePurpose, context.JK.detectOS()); } function trackLocationChange(e) { context.JK.GA.virtualPageView(location.pathname + location.search + location.hash); } function events() { $(context).resize(function() { if (resizing) { context.clearTimeout(resizing); } resizing = context.setTimeout(layout, 80); }); $('body').on('click', '[layout-link]', linkClicked); $('[layout-action="close"]').on('click', close); $('[layout-sidebar-expander]').on('click', toggleSidebar); $('[layout-panel="expanded"] [layout-panel="header"]').on('click', panelHeaderClicked); $('[layout-wizard-link]').on('click', wizardLinkClicked); $('[tab-target]').on('click', tabClicked); $(context).on('hashchange', trackLocationChange); } // public functions this.getOpts = function() { return opts; }; // used for concurrent notifications var notifyQueue = []; var firstNotification = false; var notifyDetails; this.notify = function(message, descriptor) { var $notify = $('[layout="notify"]'); if (notifyQueue.length === 0) { firstNotification = true; setNotificationInfo(message, descriptor); } notifyQueue.push({message: message, descriptor: descriptor}); // JW - speeding up the in/out parts of notify. Extending non-moving time. $notify.slideDown(250) .delay(4000) .slideUp({ duration: 400, queue: true, complete: function() { notifyDetails = notifyQueue.shift(); // shift 1 more time if this is first notification being displayed if (firstNotification) { notifyDetails = notifyQueue.shift(); firstNotification = false; } if (notifyDetails !== undefined) { setNotificationInfo(notifyDetails.message, notifyDetails.descriptor); } } }); }; function setNotificationInfo(message, descriptor) { var $notify = $('[layout="notify"]'); $('h2', $notify).text(message.title); $('p', $notify).html(message.text instanceof jQuery ? message.text.html() : message.text); if (message.icon_url) { $('#avatar', $notify).attr('src', message.icon_url); $('#avatar', $notify).show(); } else { $('#avatar', $notify).hide(); } if (message.detail) { $('div.detail', $notify).html(message.detail).show(); } else { $('div.detail', $notify).hide(); } if (descriptor) { if (descriptor.ok_text) { $('#ok-button', $notify).html(descriptor.ok_text); } else { $('#ok-button', $notify).html("OKAY"); } if (descriptor.ok_callback !== undefined) { $('#ok-button', $notify).click(function() { if (descriptor.ok_callback_args) { logger.debug("descriptor.ok_callback_args=" + descriptor.ok_callback_args); descriptor.ok_callback(descriptor.ok_callback_args); return false; } else { descriptor.ok_callback(); return false; } }); } if (descriptor.cancel_text) { $('#cancel-button', $notify).html(descriptor.cancel_text); } else { if(descriptor.no_cancel) { $('#cancel-button', $notify).hide(); } else { $('#cancel-button', $notify).html("CANCEL"); } } if (descriptor.cancel_callback !== undefined) { $('#cancel-button', $notify).click(function() { if (descriptor.cancel_callback_args) { logger.debug("descriptor.cancel_callback_args=" + descriptor.cancel_callback_args); descriptor.cancel_callback(descriptor.cancel_callback_args); return false; } else { descriptor.cancel_callback(); return false; } }); } } else { $('#ok-button', $notify).html("OKAY"); $('#cancel-button', $notify).html("CANCEL"); } } this.setWizardStep = setWizardStep; this.changeToScreen = function(screen, data) { changeToScreen(screen, data); }; this.showDialog = function(dialog) { showDialog(dialog); }; this.close = function(evt) { close(evt); }; this.closeDialog = closeDialog; /** * Given information on a grid, and a given card's grid settings, use the * margin options and return a list of [top, left, width, height] * for the cell. */ this.getCardLayout = function(gridWidth, gridHeight, gridRows, gridCols, row, col, rowspan, colspan) { var _gridWidth = gridWidth + 3*opts.gridPadding; var _gridHeight = gridHeight + 3*opts.gridPadding; var cellWidth, cellHeight, top, left, width, height; cellWidth = Math.floor((_gridWidth-2*opts.gridOuterMargin) / gridCols); cellHeight = Math.floor((_gridHeight-2*opts.gridOuterMargin) / gridRows); width = colspan * cellWidth - 2*opts.gridPadding; height = rowspan * cellHeight - 2*opts.gridPadding; top = row * cellHeight;// + opts.gridOuterMargin; // + opts.gridPadding; left = col * cellWidth;// + opts.gridOuterMargin; // + opts.gridPadding; return { top: top, left: left, width: width, height: height }; }; this.isScreenName = function(screenName) { return isScreenName(screenName); } this.bindScreen = function(screen, handler) { screenBindings[screen] = handler; }; this.bindDialog = function(dialog, handler) { dialogBindings[dialog] = handler; }; this.registerWizardStepFunction = function(stepId, showFunction) { wizardShowFunctions[stepId] = showFunction; }; this.initialize = function(inOpts) { me = this; opts = $.extend(opts, inOpts); setup(); events(); }; return this; }; }(window, jQuery));