/* * 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 = {}; function setup() { requiredStyles(); hideAll(); setInitialExpandedSidebarPanel(); sizeScreens(width, height, '[layout="screen"]', true); positionOffscreens(width, height); $('[layout="sidebar"]').show(); $('[layout="panel"]').show(); layout(); context.JK.hideCurtain(opts.animationDuration); } 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); layoutScreens(width, height); layoutSidebar(width, height); layoutHeader(width, height); layoutNotify(width, height); } function layoutCurtain(screenWidth, screenHeight) { var curtainStyle = { width: screenWidth + 'px', height: screenHeight + 'px' }; $('.curtain').css(curtainStyle); } 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; 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 }, 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(); logger.debug(searchHeight); 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 notifyStyle = { bottom: opts.gutter + opts.notifyGutter + 'px', left: opts.gutter + opts.notifyGutter + 'px', height: opts.notifyHeight - 2 * opts.notifyGutter + 'px', width: screenWidth - 2 * opts.gutter - 2 * opts.notifyGutter + 'px' }; $('[layout="notify"]').css(notifyStyle); } function requiredStyles() { var bodyStyle = { margin: '0px', padding: '0px', overflow: 'hidden' }; if (opts.allowBodyOverflow) { delete bodyStyle.overflow; } $('body').css(bodyStyle); var layoutStyle = { position: 'absolute', margin: '0px', padding: '0px' }; $('[layout]').css(layoutStyle); $('[layout="notify"]').css({"z-index": "9"}); $('[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" }); 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() { $('[layout="dialog"]').hide(); } function linkClicked(evt) { evt.preventDefault(); 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) { $(evt.currentTarget).closest('[layout]').hide(); } function screenEvent(screen, evtName, data) { if (screen in screenBindings) { if (evtName in screenBindings[screen]) { screenBindings[screen][evtName].call(me, data); } } } function changeToScreen(screen, data) { previousScreen = currentScreen; currentScreen = screen; screenEvent(previousScreen, 'beforeHide', data); screenEvent(currentScreen, 'beforeShow', data); if (currentScreen == 'home') { sidebarVisible = true; } else { sidebarVisible = false; } layout(); screenEvent(previousScreen, 'afterHide', data); screenEvent(currentScreen, 'afterShow', data); } function showDialog(dialog) { centerDialog(dialog); $('[layout-id="' + dialog + '"]').show(); } 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 events() { $(context).resize(function() { if (resizing) { context.clearTimeout(resizing); } resizing = context.setTimeout(layout, 80); }); $('[layout-link]').on('click', linkClicked); $('[layout-action="close"]').on('click', close); $('[layout-sidebar-expander]').on('click', toggleSidebar); $('[layout-panel="expanded"] [layout-panel="header"]').on('click', panelHeaderClicked); } // public functions this.getOpts = function() { return opts; }; this.notify = function(message) { var $notify = $('[layout="notify"]'); $('h1', $notify).text(message.title); $('p', $notify).text(message.text); if (message.detail) { $('div', $notify).html(message.detail).show(); } else { $('div', $notify).hide(); } $notify.show(); }; this.changeToScreen = function(screen, data) { changeToScreen(screen, data); }; /** * 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 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.bindScreen = function(screen, handler) { screenBindings[screen] = handler; }; this.initialize = function(inOpts) { me = this; opts = $.extend(opts, inOpts); setup(); events(); }; return this; }; }(window, jQuery));