UNPKG

jgiven-html-app

Version:
1,446 lines (1,272 loc) 3.47 MB
/******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/angular-chart.js/dist/angular-chart.js": /*!*************************************************************!*\ !*** ./node_modules/angular-chart.js/dist/angular-chart.js ***! \*************************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /*! * angular-chart.js - An angular.js wrapper for Chart.js * http://jtblin.github.io/angular-chart.js/ * Version: 1.0.3 * * Copyright 2016 Jerome Touffe-Blin * Released under the BSD-2-Clause license * https://github.com/jtblin/angular-chart.js/blob/master/LICENSE */ (function (factory) { 'use strict'; if (true) { // Node/CommonJS module.exports = factory( typeof angular !== 'undefined' ? angular : __webpack_require__(/*! angular */ "./node_modules/angular/index.js"), typeof Chart !== 'undefined' ? Chart : __webpack_require__(/*! chart.js */ "./node_modules/chart.js/dist/Chart.js")); } else {} }(function (angular, Chart) { 'use strict'; Chart.defaults.global.multiTooltipTemplate = '<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>'; Chart.defaults.global.tooltips.mode = 'label'; Chart.defaults.global.elements.line.borderWidth = 2; Chart.defaults.global.elements.rectangle.borderWidth = 2; Chart.defaults.global.legend.display = false; Chart.defaults.global.colors = [ '#97BBCD', // blue '#DCDCDC', // light grey '#F7464A', // red '#46BFBD', // green '#FDB45C', // yellow '#949FB1', // grey '#4D5360' // dark grey ]; var useExcanvas = typeof window.G_vmlCanvasManager === 'object' && window.G_vmlCanvasManager !== null && typeof window.G_vmlCanvasManager.initElement === 'function'; if (useExcanvas) Chart.defaults.global.animation = false; return angular.module('chart.js', []) .provider('ChartJs', ChartJsProvider) .factory('ChartJsFactory', ['ChartJs', '$timeout', ChartJsFactory]) .directive('chartBase', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory(); }]) .directive('chartLine', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('line'); }]) .directive('chartBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bar'); }]) .directive('chartHorizontalBar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('horizontalBar'); }]) .directive('chartRadar', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('radar'); }]) .directive('chartDoughnut', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('doughnut'); }]) .directive('chartPie', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('pie'); }]) .directive('chartPolarArea', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('polarArea'); }]) .directive('chartBubble', ['ChartJsFactory', function (ChartJsFactory) { return new ChartJsFactory('bubble'); }]) .name; /** * Wrapper for chart.js * Allows configuring chart js using the provider * * angular.module('myModule', ['chart.js']).config(function(ChartJsProvider) { * ChartJsProvider.setOptions({ responsive: false }); * ChartJsProvider.setOptions('Line', { responsive: true }); * }))) */ function ChartJsProvider () { var options = { responsive: true }; var ChartJs = { Chart: Chart, getOptions: function (type) { var typeOptions = type && options[type] || {}; return angular.extend({}, options, typeOptions); } }; /** * Allow to set global options during configuration */ this.setOptions = function (type, customOptions) { // If no type was specified set option for the global object if (! customOptions) { customOptions = type; options = angular.merge(options, customOptions); } else { // Set options for the specific chart options[type] = angular.merge(options[type] || {}, customOptions); } angular.merge(ChartJs.Chart.defaults, options); }; this.$get = function () { return ChartJs; }; } function ChartJsFactory (ChartJs, $timeout) { return function chart (type) { return { restrict: 'CA', scope: { chartGetColor: '=?', chartType: '=', chartData: '=?', chartLabels: '=?', chartOptions: '=?', chartSeries: '=?', chartColors: '=?', chartClick: '=?', chartHover: '=?', chartDatasetOverride: '=?' }, link: function (scope, elem/*, attrs */) { if (useExcanvas) window.G_vmlCanvasManager.initElement(elem[0]); // Order of setting "watch" matter scope.$watch('chartData', watchData, true); scope.$watch('chartSeries', watchOther, true); scope.$watch('chartLabels', watchOther, true); scope.$watch('chartOptions', watchOther, true); scope.$watch('chartColors', watchOther, true); scope.$watch('chartDatasetOverride', watchOther, true); scope.$watch('chartType', watchType, false); scope.$on('$destroy', function () { destroyChart(scope); }); scope.$on('$resize', function () { if (scope.chart) scope.chart.resize(); }); function watchData (newVal, oldVal) { if (! newVal || ! newVal.length || (Array.isArray(newVal[0]) && ! newVal[0].length)) { destroyChart(scope); return; } var chartType = type || scope.chartType; if (! chartType) return; if (scope.chart && canUpdateChart(newVal, oldVal)) return updateChart(newVal, scope); createChart(chartType, scope, elem); } function watchOther (newVal, oldVal) { if (isEmpty(newVal)) return; if (angular.equals(newVal, oldVal)) return; var chartType = type || scope.chartType; if (! chartType) return; // chart.update() doesn't work for series and labels // so we have to re-create the chart entirely createChart(chartType, scope, elem); } function watchType (newVal, oldVal) { if (isEmpty(newVal)) return; if (angular.equals(newVal, oldVal)) return; createChart(newVal, scope, elem); } } }; }; function createChart (type, scope, elem) { var options = getChartOptions(type, scope); if (! hasData(scope) || ! canDisplay(type, scope, elem, options)) return; var cvs = elem[0]; var ctx = cvs.getContext('2d'); scope.chartGetColor = getChartColorFn(scope); var data = getChartData(type, scope); // Destroy old chart if it exists to avoid ghost charts issue // https://github.com/jtblin/angular-chart.js/issues/187 destroyChart(scope); scope.chart = new ChartJs.Chart(ctx, { type: type, data: data, options: options }); scope.$emit('chart-create', scope.chart); bindEvents(cvs, scope); } function canUpdateChart (newVal, oldVal) { if (newVal && oldVal && newVal.length && oldVal.length) { return Array.isArray(newVal[0]) ? newVal.length === oldVal.length && newVal.every(function (element, index) { return element.length === oldVal[index].length; }) : oldVal.reduce(sum, 0) > 0 ? newVal.length === oldVal.length : false; } return false; } function sum (carry, val) { return carry + val; } function getEventHandler (scope, action, triggerOnlyOnChange) { var lastState = null; return function (evt) { var atEvent = scope.chart.getElementsAtEvent || scope.chart.getPointsAtEvent; if (atEvent) { var activePoints = atEvent.call(scope.chart, evt); if (triggerOnlyOnChange === false || angular.equals(lastState, activePoints) === false) { lastState = activePoints; scope[action](activePoints, evt); } } }; } function getColors (type, scope) { var colors = angular.copy(scope.chartColors || ChartJs.getOptions(type).chartColors || Chart.defaults.global.colors ); var notEnoughColors = colors.length < scope.chartData.length; while (colors.length < scope.chartData.length) { colors.push(scope.chartGetColor()); } // mutate colors in this case as we don't want // the colors to change on each refresh if (notEnoughColors) scope.chartColors = colors; return colors.map(convertColor); } function convertColor (color) { if (typeof color === 'object' && color !== null) return color; if (typeof color === 'string' && color[0] === '#') return getColor(hexToRgb(color.substr(1))); return getRandomColor(); } function getRandomColor () { var color = [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)]; return getColor(color); } function getColor (color) { return { backgroundColor: rgba(color, 0.2), pointBackgroundColor: rgba(color, 1), pointHoverBackgroundColor: rgba(color, 0.8), borderColor: rgba(color, 1), pointBorderColor: '#fff', pointHoverBorderColor: rgba(color, 1) }; } function getRandomInt (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function rgba (color, alpha) { // rgba not supported by IE8 return useExcanvas ? 'rgb(' + color.join(',') + ')' : 'rgba(' + color.concat(alpha).join(',') + ')'; } // Credit: http://stackoverflow.com/a/11508164/1190235 function hexToRgb (hex) { var bigint = parseInt(hex, 16), r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255; return [r, g, b]; } function hasData (scope) { return scope.chartData && scope.chartData.length; } function getChartColorFn (scope) { return typeof scope.chartGetColor === 'function' ? scope.chartGetColor : getRandomColor; } function getChartData (type, scope) { var colors = getColors(type, scope); return Array.isArray(scope.chartData[0]) ? getDataSets(scope.chartLabels, scope.chartData, scope.chartSeries || [], colors, scope.chartDatasetOverride) : getData(scope.chartLabels, scope.chartData, colors, scope.chartDatasetOverride); } function getDataSets (labels, data, series, colors, datasetOverride) { return { labels: labels, datasets: data.map(function (item, i) { var dataset = angular.extend({}, colors[i], { label: series[i], data: item }); if (datasetOverride && datasetOverride.length >= i) { angular.merge(dataset, datasetOverride[i]); } return dataset; }) }; } function getData (labels, data, colors, datasetOverride) { var dataset = { labels: labels, datasets: [{ data: data, backgroundColor: colors.map(function (color) { return color.pointBackgroundColor; }), hoverBackgroundColor: colors.map(function (color) { return color.backgroundColor; }) }] }; if (datasetOverride) { angular.merge(dataset.datasets[0], datasetOverride); } return dataset; } function getChartOptions (type, scope) { return angular.extend({}, ChartJs.getOptions(type), scope.chartOptions); } function bindEvents (cvs, scope) { cvs.onclick = scope.chartClick ? getEventHandler(scope, 'chartClick', false) : angular.noop; cvs.onmousemove = scope.chartHover ? getEventHandler(scope, 'chartHover', true) : angular.noop; } function updateChart (values, scope) { if (Array.isArray(scope.chartData[0])) { scope.chart.data.datasets.forEach(function (dataset, i) { dataset.data = values[i]; }); } else { scope.chart.data.datasets[0].data = values; } scope.chart.update(); scope.$emit('chart-update', scope.chart); } function isEmpty (value) { return ! value || (Array.isArray(value) && ! value.length) || (typeof value === 'object' && ! Object.keys(value).length); } function canDisplay (type, scope, elem, options) { // TODO: check parent? if (options.responsive && elem[0].clientHeight === 0) { $timeout(function () { createChart(type, scope, elem); }, 50, false); return false; } return true; } function destroyChart(scope) { if(! scope.chart) return; scope.chart.destroy(); scope.$emit('chart-destroy', scope.chart); } } })); /***/ }), /***/ "./node_modules/angular-foundation/mm-foundation-tpls.js": /*!***************************************************************!*\ !*** ./node_modules/angular-foundation/mm-foundation-tpls.js ***! \***************************************************************/ /***/ (() => { /* * angular-mm-foundation * http://pineconellc.github.io/angular-foundation/ * Version: 0.8.0 - 2015-10-13 * License: MIT * (c) Pinecone, LLC */ angular.module("mm.foundation", ["mm.foundation.tpls", "mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.mediaQueries","mm.foundation.dropdownToggle","mm.foundation.interchange","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.topbar","mm.foundation.tour","mm.foundation.typeahead"]); angular.module("mm.foundation.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/topbar/has-dropdown.html","template/topbar/toggle-top-bar.html","template/topbar/top-bar-dropdown.html","template/topbar/top-bar-section.html","template/topbar/top-bar.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); angular.module('mm.foundation.accordion', []) .constant('accordionConfig', { closeOthers: true }) .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; if ( closeOthers ) { angular.forEach(this.groups, function (group) { if ( group !== openGroup ) { group.isOpen = false; } }); } }; // This is called from the accordion-group directive to add itself to the accordion this.addGroup = function(groupScope) { var that = this; this.groups.push(groupScope); groupScope.$on('$destroy', function (event) { that.removeGroup(groupScope); }); }; // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); if ( index !== -1 ) { this.groups.splice(index, 1); } }; }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. .directive('accordion', function () { return { restrict:'EA', controller:'AccordionController', transclude: true, replace: false, templateUrl: 'template/accordion/accordion.html' }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion .directive('accordionGroup', ['$parse', function($parse) { return { require:'^accordion', // We need this directive to be inside an accordion restrict:'EA', transclude:true, // It transcludes the contents of the directive into the template replace: true, // The element containing the directive will be replaced with the template templateUrl:'template/accordion/accordion-group.html', scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope controller: function() { this.setHeading = function(element) { this.heading = element; }; }, link: function(scope, element, attrs, accordionCtrl) { var getIsOpen, setIsOpen; accordionCtrl.addGroup(scope); scope.isOpen = false; if ( attrs.isOpen ) { getIsOpen = $parse(attrs.isOpen); setIsOpen = getIsOpen.assign; scope.$parent.$watch(getIsOpen, function(value) { scope.isOpen = !!value; }); } scope.$watch('isOpen', function(value) { if ( value ) { accordionCtrl.closeOthers(scope); } if ( setIsOpen ) { setIsOpen(scope.$parent, value); } }); } }; }]) // Use accordion-heading below an accordion-group to provide a heading containing HTML // <accordion-group> // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading> // </accordion-group> .directive('accordionHeading', function() { return { restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, require: '^accordionGroup', compile: function(element, attr, transclude) { return function link(scope, element, attr, accordionGroupCtrl) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] accordionGroupCtrl.setHeading(transclude(scope, function() {})); }; } }; }) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element // <div class="accordion-group"> // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div> // ... // </div> .directive('accordionTransclude', function() { return { require: '^accordionGroup', link: function(scope, element, attr, controller) { scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { if ( heading ) { element.html(''); element.append(heading); } }); } }; }); angular.module("mm.foundation.alert", []) .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { $scope.closeable = 'close' in $attrs && typeof $attrs.close !== "undefined"; }]) .directive('alert', function () { return { restrict:'EA', controller:'AlertController', templateUrl:'template/alert/alert.html', transclude:true, replace:true, scope: { type: '=', close: '&' } }; }); angular.module('mm.foundation.bindHtml', []) .directive('bindHtmlUnsafe', function () { return function (scope, element, attr) { element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { element.html(value || ''); }); }; }); angular.module('mm.foundation.buttons', []) .constant('buttonConfig', { activeClass: 'active', toggleEvent: 'click' }) .controller('ButtonsController', ['buttonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass; this.toggleEvent = buttonConfig.toggleEvent; }]) .directive('btnRadio', function () { return { require: ['btnRadio', 'ngModel'], controller: 'ButtonsController', link: function (scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; //model -> UI ngModelCtrl.$render = function () { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); }; //ui->model element.bind(buttonsCtrl.toggleEvent, function () { if (!element.hasClass(buttonsCtrl.activeClass)) { scope.$apply(function () { ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio)); ngModelCtrl.$render(); }); } }); } }; }) .directive('btnCheckbox', function () { return { require: ['btnCheckbox', 'ngModel'], controller: 'ButtonsController', link: function (scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } function getFalseValue() { return getCheckboxValue(attrs.btnCheckboxFalse, false); } function getCheckboxValue(attributeValue, defaultValue) { var val = scope.$eval(attributeValue); return angular.isDefined(val) ? val : defaultValue; } //model -> UI ngModelCtrl.$render = function () { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model element.bind(buttonsCtrl.toggleEvent, function () { scope.$apply(function () { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); }); } }; }); angular.module('mm.foundation.position', []) /** * A set of utility methods that can be use to retrieve position of DOM elements. * It is meant to be used where we need to absolute-position DOM elements in * relation to other, existing elements (this is the case for tooltips, popovers, * typeahead suggestions etc.). */ .factory('$position', ['$document', '$window', function ($document, $window) { function getStyle(el, cssprop) { if (el.currentStyle) { //IE return el.currentStyle[cssprop]; } else if ($window.getComputedStyle) { return $window.getComputedStyle(el)[cssprop]; } // finally try and get inline style return el.style[cssprop]; } /** * Checks if a given element is statically positioned * @param element - raw DOM element */ function isStaticPositioned(element) { return (getStyle(element, "position") || 'static' ) === 'static'; } /** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ var parentOffsetEl = function (element) { var docDomEl = $document[0]; var offsetParent = element.offsetParent || docDomEl; while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { offsetParent = offsetParent.offsetParent; } return offsetParent || docDomEl; }; return { /** * Provides read-only equivalent of jQuery's position function: * http://api.jquery.com/position/ */ position: function (element) { var elBCR = this.offset(element); var offsetParentBCR = { top: 0, left: 0 }; var offsetParentEl = parentOffsetEl(element[0]); if (offsetParentEl != $document[0]) { offsetParentBCR = this.offset(angular.element(offsetParentEl)); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; } var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left }; }, /** * Provides read-only equivalent of jQuery's offset function: * http://api.jquery.com/offset/ */ offset: function (element) { var boundingClientRect = element[0].getBoundingClientRect(); return { width: boundingClientRect.width || element.prop('offsetWidth'), height: boundingClientRect.height || element.prop('offsetHeight'), top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop), left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) }; } }; }]); angular.module("mm.foundation.mediaQueries", []) .factory('matchMedia', ['$document', '$window', function($document, $window) { // MatchMedia for IE <= 9 return $window.matchMedia || (function matchMedia(doc, undefined){ var bool, docElem = doc.documentElement, refNode = docElem.firstElementChild || docElem.firstChild, // fakeBody required for <FF4 when executed in <head> fakeBody = doc.createElement("body"), div = doc.createElement("div"); div.id = "mq-test-1"; div.style.cssText = "position:absolute;top:-100em"; fakeBody.style.background = "none"; fakeBody.appendChild(div); return function (q) { div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>"; docElem.insertBefore(fakeBody, refNode); bool = div.offsetWidth === 42; docElem.removeChild(fakeBody); return { matches: bool, media: q }; }; }($document[0])); }]) .factory('mediaQueries', ['$document', 'matchMedia', function($document, matchMedia) { var head = angular.element($document[0].querySelector('head')); head.append('<meta class="foundation-mq-topbar" />'); head.append('<meta class="foundation-mq-small" />'); head.append('<meta class="foundation-mq-medium" />'); head.append('<meta class="foundation-mq-large" />'); var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g; var queries = { topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''), small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''), medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''), large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '') }; return { topbarBreakpoint: function () { return !matchMedia(queries.topbar).matches; }, small: function () { return matchMedia(queries.small).matches; }, medium: function () { return matchMedia(queries.medium).matches; }, large: function () { return matchMedia(queries.large).matches; } }; }]); /* * dropdownToggle - Provides dropdown menu functionality * @restrict class or attribute * @example: <a dropdown-toggle="#dropdown-menu">My Dropdown Menu</a> <ul id="dropdown-menu" class="f-dropdown"> <li ng-repeat="choice in dropChoices"> <a ng-href="{{choice.href}}">{{choice.text}}</a> </li> </ul> */ angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position', 'mm.foundation.mediaQueries' ]) .controller('DropdownToggleController', ['$scope', '$attrs', 'mediaQueries', function($scope, $attrs, mediaQueries) { this.small = function() { return mediaQueries.small() && !mediaQueries.medium(); }; }]) .directive('dropdownToggle', ['$document', '$window', '$location', '$position', function ($document, $window, $location, $position) { var openElement = null, closeMenu = angular.noop; return { restrict: 'CA', controller: 'DropdownToggleController', link: function(scope, element, attrs, controller) { var parent = element.parent(), dropdown = angular.element($document[0].querySelector(attrs.dropdownToggle)); var parentHasDropdown = function() { return parent.hasClass('has-dropdown'); }; var onClick = function (event) { dropdown = angular.element($document[0].querySelector(attrs.dropdownToggle)); var elementWasOpen = (element === openElement); event.preventDefault(); event.stopPropagation(); if (!!openElement) { closeMenu(); } if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) { dropdown.css('display', 'block'); // We display the element so that offsetParent is populated dropdown.addClass('f-open-dropdown'); var offset = $position.offset(element); var parentOffset = $position.offset(angular.element(dropdown[0].offsetParent)); var dropdownWidth = dropdown.prop('offsetWidth'); var css = { top: offset.top - parentOffset.top + offset.height + 'px' }; if (controller.small()) { css.left = Math.max((parentOffset.width - dropdownWidth) / 2, 8) + 'px'; css.position = 'absolute'; css.width = '95%'; css['max-width'] = 'none'; } else { var left = Math.round(offset.left - parentOffset.left); var rightThreshold = $window.innerWidth - dropdownWidth - 8; if (left > rightThreshold) { left = rightThreshold; dropdown.removeClass('left').addClass('right'); } css.left = left + 'px'; css.position = null; css['max-width'] = null; } dropdown.css(css); element.addClass('expanded'); if (parentHasDropdown()) { parent.addClass('hover'); } openElement = element; closeMenu = function (event) { $document.off('click', closeMenu); dropdown.css('display', 'none'); dropdown.removeClass('f-open-dropdown'); element.removeClass('expanded'); closeMenu = angular.noop; openElement = null; if (parent.hasClass('hover')) { parent.removeClass('hover'); } }; $document.on('click', closeMenu); } }; if (dropdown) { dropdown.css('display', 'none'); } scope.$watch('$location.path', function() { closeMenu(); }); element.on('click', onClick); element.on('$destroy', function() { element.off('click', onClick); }); } }; }]); /** * @ngdoc service * @name mm.foundation.interchange * @description * * Package containing all services and directives * about the `interchange` module */ angular.module('mm.foundation.interchange', ['mm.foundation.mediaQueries']) /** * @ngdoc function * @name mm.foundation.interchange.interchageQuery * @function interchageQuery * @description * * this service inject meta tags objects in the head * to get the list of media queries from Foundation * stylesheets. * * @return {object} Queries list name => mediaQuery */ .factory('interchangeQueries', ['$document', function ($document) { var element, mediaSize, formatList = { 'default': 'only screen', landscape : 'only screen and (orientation: landscape)', portrait : 'only screen and (orientation: portrait)', retina : 'only screen and (-webkit-min-device-pixel-ratio: 2),' + 'only screen and (min--moz-device-pixel-ratio: 2),' + 'only screen and (-o-min-device-pixel-ratio: 2/1),' + 'only screen and (min-device-pixel-ratio: 2),' + 'only screen and (min-resolution: 192dpi),' + 'only screen and (min-resolution: 2dppx)' }, classPrefix = 'foundation-mq-', classList = ['small', 'medium', 'large', 'xlarge', 'xxlarge'], head = angular.element($document[0].querySelector('head')); for (var i = 0; i < classList.length; i++) { head.append('<meta class="' + classPrefix + classList[i] + '" />'); element = getComputedStyle(head[0].querySelector('meta.' + classPrefix + classList[i])); mediaSize = element.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g, ''); formatList[classList[i]] = mediaSize; } return formatList; }]) /** * @ngdoc function * @name mm.foundation.interchange.interchangeQueriesManager * @function interchangeQueriesManager * @description * * interface to add and remove named queries * in the interchangeQueries list */ .factory('interchangeQueriesManager', ['interchangeQueries', function (interchangeQueries) { return { /** * @ngdoc method * @name interchangeQueriesManager#add * @methodOf mm.foundation.interchange.interchangeQueriesManager * @description * * Add a custom media query in the `interchangeQueries` * factory. This method does not allow to update an existing * media query. * * @param {string} name MediaQuery name * @param {string} media MediaQuery * @returns {boolean} True if the insert is a success */ add: function (name, media) { if (!name || !media || !angular.isString(name) || !angular.isString(media) || !!interchangeQueries[name]) { return false; } interchangeQueries[name] = media; return true; } }; }]) /** * @ngdoc function * @name mm.foundation.interchange.interchangeTools * @function interchangeTools * @description * * Tools to help with the `interchange` module. */ .factory('interchangeTools', ['$window', 'matchMedia', 'interchangeQueries', function ($window, matchMedia, namedQueries) { /** * @ngdoc method * @name interchangeTools#parseAttribute * @methodOf mm.foundation.interchange.interchangeTools * @description * * Attribute parser to transform an `interchange` attribute * value to an object with media query (name or query) as key, * and file to use as value. * * ``` * { * small: 'bridge-500.jpg', * large: 'bridge-1200.jpg' * } * ``` * * @param {string} value Interchange query string * @returns {object} Attribute parsed */ var parseAttribute = function (value) { var raw = value.split(/\[(.*?)\]/), i = raw.length, breaker = /^(.+)\,\ \((.+)\)$/, breaked, output = {}; while (i--) { if (raw[i].replace(/[\W\d]+/, '').length > 4) { breaked = breaker.exec(raw[i]); if (!!breaked && breaked.length === 3) { output[breaked[2]] = breaked[1]; } } } return output; }; /** * @ngdoc method * @name interchangeTools#findCurrentMediaFile * @methodOf mm.foundation.interchange.interchangeTools * @description * * Find the current item to display from a file list * (object returned by `parseAttribute`) and the * current page dimensions. * * ``` * { * small: 'bridge-500.jpg', * large: 'bridge-1200.jpg' * } * ``` * * @param {object} files Parsed version of `interchange` attribute * @returns {string} File to display (or `undefined`) */ var findCurrentMediaFile = function (files) { var file, media, match; for (file in files) { media = namedQueries[file] || file; match = matchMedia(media); if (match.matches) { return files[file]; } } return; }; return { parseAttribute: parseAttribute, findCurrentMediaFile: findCurrentMediaFile }; }]) /** * @ngdoc directive * @name mm.foundation.interchange.directive:interchange * @restrict A * @element DIV|IMG * @priority 450 * @scope true * @description * * Interchange directive, following the same features as * ZURB documentation. The directive is splitted in 3 parts. * * 1. This directive use `compile` and not `link` for a simple * reason: if the method is applied on a DIV element to * display a template, the compile method will inject an ng-include. * Because using a `templateUrl` or `template` to do it wouldn't * be appropriate for all cases (`IMG` or dynamic backgrounds). * And doing it in `link` is too late to be applied. * * 2. In the `compile:post`, the attribute is parsed to find * out the type of content to display. * * 3. At the start and on event `resize`, the method `replace` * is called to reevaluate which file is supposed to be displayed * and update the value if necessary. The methd will also * trigger a `replace` event. */ .directive('interchange', ['$window', '$rootScope', 'interchangeTools', function ($window, $rootScope, interchangeTools) { var pictureFilePattern = /[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i; return { restrict: 'A', scope: true, priority: 450, compile: function compile($element, attrs) { // Set up the attribute to update if ($element[0].nodeName === 'DIV' && !pictureFilePattern.test(attrs.interchange)) { $element.html('<ng-include src="currentFile"></ng-include>'); } return { pre: function preLink($scope, $element, attrs) {}, post: function postLink($scope, $element, attrs) { var currentFile, nodeName; // Set up the attribute to update nodeName = $element && $element[0] && $element[0].nodeName; $scope.fileMap = interchangeTools.parseAttribute(attrs.interchange); // Find the type of interchange switch (nodeName) { case 'DIV': // If the tag is a div, we test the current file to see if it's picture currentFile = interchangeTools.findCurrentMediaFile($scope.fileMap); if (/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(currentFile)) { $scope.type = 'background'; } else { $scope.type = 'include'; } break; case 'IMG': $scope.type = 'image'; break; default: return; } var replace = function (e) { // The the new file to display (exit if the same) var currentFile = interchangeTools.findCurrentMediaFile($scope.fileMap); if (!!$scope.currentFile && $scope.currentFile === currentFile) { return; } // Set up the new file $scope.currentFile = currentFile; switch ($scope.type) { case 'image': $element.attr('src', $scope.currentFile); break; case 'background': $element.css('background-image', 'url(' + $scope.currentFile + ')'); break; } // Trigger events $rootScope.$emit('replace', $element, $scope); if (!!e) { $scope.$apply(); } }; // Start replace(); $window.addEventListener('resize', replace); $scope.$on('$destroy', function () { $window.removeEventListener('resize', replace); }); } }; } }; }]); angular.module('mm.foundation.transition', []) /** * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. * @param {DOMElement} element The DOMElement that will be animated. * @param {string|object|function} trigger The thing that will cause the transition to start: * - As a string, it represents the css class to be added to the element. * - As an object, it represents a hash of style attributes to be applied to the element. * - As a function, it represents a function to be called that will cause the transition to occur. * @return {Promise} A promise that is resolved when the transition finishes. */ .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { var $transition = function(element, trigger, options) { options = options || {}; var deferred = $q.defer(); var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"]; var transitionEndHandler = function(event) { $rootScope.$apply(function() { element.unbind(endEventName, transitionEndHandler); deferred.resolve(element); }); }; if (endEventName) { element.bind(endEventName, transitionEndHandler); } // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur $timeout(function() { if ( angular.isString(trigger) ) { element.addClass(trigger); } else if ( angular.isFunction(trigger) ) { trigger(element); } else if ( angular.isObject(trigger) ) { element.css(trigger); } //If browser does not support transitions, instantly resolve if ( !endEventName ) { deferred.resolve(element); } }); // Add our custom cancel function to the promise that is returned // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, // i.e. it will therefore never raise a transitionEnd event for that transition deferred.promise.cancel = function() { if ( endEventName ) { element.unbind(endEventName, transitionEndHandler); } deferred.reject('Transition cancelled'); }; return deferred.promise; }; // Work out the name of the transitionEnd event var transElement = document.createElement('trans'); var transitionEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'transition': 'transitionend' }; var animationEndEventNames = { 'WebkitTransition': 'webkitAnimationEnd', 'MozTransition': 'animationend', 'OTransition': 'oAnimationEnd', 'transition': 'animationend' }; function findEndEventName(endEventNames) { for (var name in endEventNames){ if (transElement.style[name] !== undefined) { return endEventNames[name]; } } } $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); $transition.animationEndEventName = findEndEventName(animationEndEventNames); return $transition; }]); angular.module('mm.foundation.modal', ['mm.foundation.transition']) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ .factory('$$stackedMap', function () { return { createNew: function () { var stack = []; return { add: function (key, value) { stack.push({ key: key, value: value }); }, get: function (key) { for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { return stack[i]; } } }, keys: function() { var keys = []; for (var i = 0; i < stack.length; i++) { keys.push(stack[i].key); } return keys; }, top: function () { return stack[stack.length - 1]; }, remove: function (key) { var idx = -1; for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, removeTop: function () { return stack.splice(stack.length - 1, 1)[0]; }, length: function () { return stack.length; } }; } }; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('modalBackdrop', ['$modalStack', '$timeout', function ($modalStack, $timeout) { return { restrict: 'EA', replace: true, templateUrl: 'template/modal/backdrop.html', link: function (scope) { scope.animate = false; //trigger CSS transitions $timeout(function () { scope.animate = true; }); scope.close = function (evt) { var modal = $modalStack.getTop(); if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; } }; }]) .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { return { restrict: 'EA', scope: { index: '@', animate: '=' }, replace: true, transclude: true, templateUrl: 'template/modal/window.html', link: function (scope, element, attrs) { scope.windowClass = attrs.windowClass || ''; $timeout(function () { // trigger CSS transitions scope.animate = true; // If the modal contains any autofocus elements refocus onto the first one if (element[0].querySelectorAll('[autofocus]').length > 0) { element[0].querySelectorAll('[autofocus]')[0].focus(); } else{ // otherwise focus the freshly-opened modal element[0].focus(); } }); } }; }]) .factory('$modalStack', ['$window', '$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', function ($window, $transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope, cssTop; var openedWindows = $$stackedMap.createNew(); var $modalStack = {}; function backdropIndex() { var topBackdropIndex = -1; var opened = openedWindows.keys(); for (var i = 0; i < opened.length; i++) { if (openedWindows.get(opened[i]).value.backdrop) { topBackdropIndex = i; } } return topBackdropIndex; } $rootScope.$watch(backdropIndex, function(newBackdropIndex){ if (backdropScope) { backdropScope.index = newBackdropIndex; } }); function removeModalWindow(modalInstance) { var parent = $document.find(modalInstance.options.parent).eq(0); var modalWindow = openedWindows.get(modalInstance).value; //clean up the stack openedWindows.remove(modalInstance); //remove window DOM element removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { modalWindow.modalScope.$destroy(); parent.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); checkRemoveBackdrop(); }); } function checkRemoveBackdrop() { //remove backdrop if no longer needed if (backdropDomEl && backdropIndex() == -1) { var backdropScopeRef = backdropScope; removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { backdropScopeRef.$destroy(); backdropScopeRef = null; }); backdropDomEl = undefined; backdropScope = undefined; } } function removeAfterAnimate(domEl, scope, emulateTime, done) { // Closing animation scope.animate = false; var transitionEndEventName = $transition.transitionEndEventName; if (transitionEndEventName) { // transition out var timeout = $timeout(afterAnimating, emulateTime); domEl.bind(transitionEndEventName, function () { $timeout.cancel(timeout); afterAnimating(); scope.$apply(); }); } else { // Ensure this call is async $timeout(afterAnimating, 0); }