jgiven-html-app
Version:
The HTML App of JGiven and JsGiven
1,446 lines (1,272 loc) • 3.47 MB
JavaScript
/******/ (() => { // 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 = "­<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);
}