angularjs-foundation-rails 0.3.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3291 @@
1
+ /*
2
+ * angular-mm-foundation
3
+ * http://pineconellc.github.io/angular-foundation/
4
+
5
+ * Version: 0.3.1 - 2014-08-19
6
+ * License: MIT
7
+ * (c) Pinecone, LLC
8
+ */
9
+ angular.module("mm.foundation", ["mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","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"]);
10
+ angular.module('mm.foundation.accordion', [])
11
+
12
+ .constant('accordionConfig', {
13
+ closeOthers: true
14
+ })
15
+
16
+ .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
17
+
18
+ // This array keeps track of the accordion groups
19
+ this.groups = [];
20
+
21
+ // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
22
+ this.closeOthers = function(openGroup) {
23
+ var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
24
+ if ( closeOthers ) {
25
+ angular.forEach(this.groups, function (group) {
26
+ if ( group !== openGroup ) {
27
+ group.isOpen = false;
28
+ }
29
+ });
30
+ }
31
+ };
32
+
33
+ // This is called from the accordion-group directive to add itself to the accordion
34
+ this.addGroup = function(groupScope) {
35
+ var that = this;
36
+ this.groups.push(groupScope);
37
+
38
+ groupScope.$on('$destroy', function (event) {
39
+ that.removeGroup(groupScope);
40
+ });
41
+ };
42
+
43
+ // This is called from the accordion-group directive when to remove itself
44
+ this.removeGroup = function(group) {
45
+ var index = this.groups.indexOf(group);
46
+ if ( index !== -1 ) {
47
+ this.groups.splice(this.groups.indexOf(group), 1);
48
+ }
49
+ };
50
+
51
+ }])
52
+
53
+ // The accordion directive simply sets up the directive controller
54
+ // and adds an accordion CSS class to itself element.
55
+ .directive('accordion', function () {
56
+ return {
57
+ restrict:'EA',
58
+ controller:'AccordionController',
59
+ transclude: true,
60
+ replace: false,
61
+ templateUrl: 'template/accordion/accordion.html'
62
+ };
63
+ })
64
+
65
+ // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
66
+ .directive('accordionGroup', ['$parse', function($parse) {
67
+ return {
68
+ require:'^accordion', // We need this directive to be inside an accordion
69
+ restrict:'EA',
70
+ transclude:true, // It transcludes the contents of the directive into the template
71
+ replace: true, // The element containing the directive will be replaced with the template
72
+ templateUrl:'template/accordion/accordion-group.html',
73
+ scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
74
+ controller: function() {
75
+ this.setHeading = function(element) {
76
+ this.heading = element;
77
+ };
78
+ },
79
+ link: function(scope, element, attrs, accordionCtrl) {
80
+ var getIsOpen, setIsOpen;
81
+
82
+ accordionCtrl.addGroup(scope);
83
+
84
+ scope.isOpen = false;
85
+
86
+ if ( attrs.isOpen ) {
87
+ getIsOpen = $parse(attrs.isOpen);
88
+ setIsOpen = getIsOpen.assign;
89
+
90
+ scope.$parent.$watch(getIsOpen, function(value) {
91
+ scope.isOpen = !!value;
92
+ });
93
+ }
94
+
95
+ scope.$watch('isOpen', function(value) {
96
+ if ( value ) {
97
+ accordionCtrl.closeOthers(scope);
98
+ }
99
+ if ( setIsOpen ) {
100
+ setIsOpen(scope.$parent, value);
101
+ }
102
+ });
103
+ }
104
+ };
105
+ }])
106
+
107
+ // Use accordion-heading below an accordion-group to provide a heading containing HTML
108
+ // <accordion-group>
109
+ // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
110
+ // </accordion-group>
111
+ .directive('accordionHeading', function() {
112
+ return {
113
+ restrict: 'EA',
114
+ transclude: true, // Grab the contents to be used as the heading
115
+ template: '', // In effect remove this element!
116
+ replace: true,
117
+ require: '^accordionGroup',
118
+ compile: function(element, attr, transclude) {
119
+ return function link(scope, element, attr, accordionGroupCtrl) {
120
+ // Pass the heading to the accordion-group controller
121
+ // so that it can be transcluded into the right place in the template
122
+ // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
123
+ accordionGroupCtrl.setHeading(transclude(scope, function() {}));
124
+ };
125
+ }
126
+ };
127
+ })
128
+
129
+ // Use in the accordion-group template to indicate where you want the heading to be transcluded
130
+ // You must provide the property on the accordion-group controller that will hold the transcluded element
131
+ // <div class="accordion-group">
132
+ // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
133
+ // ...
134
+ // </div>
135
+ .directive('accordionTransclude', function() {
136
+ return {
137
+ require: '^accordionGroup',
138
+ link: function(scope, element, attr, controller) {
139
+ scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
140
+ if ( heading ) {
141
+ element.html('');
142
+ element.append(heading);
143
+ }
144
+ });
145
+ }
146
+ };
147
+ });
148
+
149
+ angular.module("mm.foundation.alert", [])
150
+
151
+ .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
152
+ $scope.closeable = 'close' in $attrs;
153
+ }])
154
+
155
+ .directive('alert', function () {
156
+ return {
157
+ restrict:'EA',
158
+ controller:'AlertController',
159
+ templateUrl:'template/alert/alert.html',
160
+ transclude:true,
161
+ replace:true,
162
+ scope: {
163
+ type: '=',
164
+ close: '&'
165
+ }
166
+ };
167
+ });
168
+
169
+ angular.module('mm.foundation.bindHtml', [])
170
+
171
+ .directive('bindHtmlUnsafe', function () {
172
+ return function (scope, element, attr) {
173
+ element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
174
+ scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
175
+ element.html(value || '');
176
+ });
177
+ };
178
+ });
179
+
180
+ angular.module('mm.foundation.buttons', [])
181
+
182
+ .constant('buttonConfig', {
183
+ activeClass: 'active',
184
+ toggleEvent: 'click'
185
+ })
186
+
187
+ .controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
188
+ this.activeClass = buttonConfig.activeClass;
189
+ this.toggleEvent = buttonConfig.toggleEvent;
190
+ }])
191
+
192
+ .directive('btnRadio', function () {
193
+ return {
194
+ require: ['btnRadio', 'ngModel'],
195
+ controller: 'ButtonsController',
196
+ link: function (scope, element, attrs, ctrls) {
197
+ var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
198
+
199
+ //model -> UI
200
+ ngModelCtrl.$render = function () {
201
+ element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
202
+ };
203
+
204
+ //ui->model
205
+ element.bind(buttonsCtrl.toggleEvent, function () {
206
+ if (!element.hasClass(buttonsCtrl.activeClass)) {
207
+ scope.$apply(function () {
208
+ ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio));
209
+ ngModelCtrl.$render();
210
+ });
211
+ }
212
+ });
213
+ }
214
+ };
215
+ })
216
+
217
+ .directive('btnCheckbox', function () {
218
+ return {
219
+ require: ['btnCheckbox', 'ngModel'],
220
+ controller: 'ButtonsController',
221
+ link: function (scope, element, attrs, ctrls) {
222
+ var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
223
+
224
+ function getTrueValue() {
225
+ return getCheckboxValue(attrs.btnCheckboxTrue, true);
226
+ }
227
+
228
+ function getFalseValue() {
229
+ return getCheckboxValue(attrs.btnCheckboxFalse, false);
230
+ }
231
+
232
+ function getCheckboxValue(attributeValue, defaultValue) {
233
+ var val = scope.$eval(attributeValue);
234
+ return angular.isDefined(val) ? val : defaultValue;
235
+ }
236
+
237
+ //model -> UI
238
+ ngModelCtrl.$render = function () {
239
+ element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
240
+ };
241
+
242
+ //ui->model
243
+ element.bind(buttonsCtrl.toggleEvent, function () {
244
+ scope.$apply(function () {
245
+ ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
246
+ ngModelCtrl.$render();
247
+ });
248
+ });
249
+ }
250
+ };
251
+ });
252
+
253
+ angular.module('mm.foundation.position', [])
254
+
255
+ /**
256
+ * A set of utility methods that can be use to retrieve position of DOM elements.
257
+ * It is meant to be used where we need to absolute-position DOM elements in
258
+ * relation to other, existing elements (this is the case for tooltips, popovers,
259
+ * typeahead suggestions etc.).
260
+ */
261
+ .factory('$position', ['$document', '$window', function ($document, $window) {
262
+
263
+ function getStyle(el, cssprop) {
264
+ if (el.currentStyle) { //IE
265
+ return el.currentStyle[cssprop];
266
+ } else if ($window.getComputedStyle) {
267
+ return $window.getComputedStyle(el)[cssprop];
268
+ }
269
+ // finally try and get inline style
270
+ return el.style[cssprop];
271
+ }
272
+
273
+ /**
274
+ * Checks if a given element is statically positioned
275
+ * @param element - raw DOM element
276
+ */
277
+ function isStaticPositioned(element) {
278
+ return (getStyle(element, "position") || 'static' ) === 'static';
279
+ }
280
+
281
+ /**
282
+ * returns the closest, non-statically positioned parentOffset of a given element
283
+ * @param element
284
+ */
285
+ var parentOffsetEl = function (element) {
286
+ var docDomEl = $document[0];
287
+ var offsetParent = element.offsetParent || docDomEl;
288
+ while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
289
+ offsetParent = offsetParent.offsetParent;
290
+ }
291
+ return offsetParent || docDomEl;
292
+ };
293
+
294
+ return {
295
+ /**
296
+ * Provides read-only equivalent of jQuery's position function:
297
+ * http://api.jquery.com/position/
298
+ */
299
+ position: function (element) {
300
+ var elBCR = this.offset(element);
301
+ var offsetParentBCR = { top: 0, left: 0 };
302
+ var offsetParentEl = parentOffsetEl(element[0]);
303
+ if (offsetParentEl != $document[0]) {
304
+ offsetParentBCR = this.offset(angular.element(offsetParentEl));
305
+ offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
306
+ offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
307
+ }
308
+
309
+ var boundingClientRect = element[0].getBoundingClientRect();
310
+ return {
311
+ width: boundingClientRect.width || element.prop('offsetWidth'),
312
+ height: boundingClientRect.height || element.prop('offsetHeight'),
313
+ top: elBCR.top - offsetParentBCR.top,
314
+ left: elBCR.left - offsetParentBCR.left
315
+ };
316
+ },
317
+
318
+ /**
319
+ * Provides read-only equivalent of jQuery's offset function:
320
+ * http://api.jquery.com/offset/
321
+ */
322
+ offset: function (element) {
323
+ var boundingClientRect = element[0].getBoundingClientRect();
324
+ return {
325
+ width: boundingClientRect.width || element.prop('offsetWidth'),
326
+ height: boundingClientRect.height || element.prop('offsetHeight'),
327
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
328
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
329
+ };
330
+ }
331
+ };
332
+ }]);
333
+
334
+ /*
335
+ * dropdownToggle - Provides dropdown menu functionality
336
+ * @restrict class or attribute
337
+ * @example:
338
+
339
+ <a dropdown-toggle="#dropdown-menu">My Dropdown Menu</a>
340
+ <ul id="dropdown-menu" class="f-dropdown">
341
+ <li ng-repeat="choice in dropChoices">
342
+ <a ng-href="{{choice.href}}">{{choice.text}}</a>
343
+ </li>
344
+ </ul>
345
+ */
346
+ angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position' ])
347
+
348
+ .directive('dropdownToggle', ['$document', '$location', '$position', function ($document, $location, $position) {
349
+ var openElement = null,
350
+ closeMenu = angular.noop;
351
+ return {
352
+ restrict: 'CA',
353
+ scope: {
354
+ dropdownToggle: '@'
355
+ },
356
+ link: function(scope, element, attrs) {
357
+ var dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));
358
+
359
+ scope.$watch('$location.path', function() { closeMenu(); });
360
+ element.bind('click', function (event) {
361
+ dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));
362
+ var elementWasOpen = (element === openElement);
363
+
364
+ event.preventDefault();
365
+ event.stopPropagation();
366
+
367
+ if (!!openElement) {
368
+ closeMenu();
369
+ }
370
+
371
+ if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
372
+ dropdown.css('display', 'block');
373
+
374
+ var offset = $position.offset(element);
375
+ var parentOffset = $position.offset(angular.element(dropdown[0].offsetParent));
376
+
377
+ dropdown.css({
378
+ left: offset.left - parentOffset.left + 'px',
379
+ top: offset.top - parentOffset.top + offset.height + 'px'
380
+ });
381
+
382
+ openElement = element;
383
+ closeMenu = function (event) {
384
+ $document.unbind('click', closeMenu);
385
+ dropdown.css('display', 'none');
386
+ closeMenu = angular.noop;
387
+ openElement = null;
388
+ };
389
+ $document.bind('click', closeMenu);
390
+ }
391
+ });
392
+
393
+ if (dropdown) {
394
+ dropdown.css('display', 'none');
395
+ }
396
+ }
397
+ };
398
+ }]);
399
+
400
+ /**
401
+ * @ngdoc service
402
+ * @name mm.foundation.interchange
403
+ * @description
404
+ *
405
+ * Package containing all services and directives
406
+ * about the `interchange` module
407
+ */
408
+ angular.module('mm.foundation.interchange', [])
409
+
410
+ /**
411
+ * @ngdoc function
412
+ * @name mm.foundation.interchange.interchageQuery
413
+ * @function interchageQuery
414
+ * @description
415
+ *
416
+ * this service inject meta tags objects in the head
417
+ * to get the list of media queries from Foundation
418
+ * stylesheets.
419
+ *
420
+ * @return {object} Queries list name => mediaQuery
421
+ */
422
+ .factory('interchangeQueries', ['$document', function ($document) {
423
+ var element,
424
+ mediaSize,
425
+ formatList = {
426
+ 'default': 'only screen',
427
+ landscape : 'only screen and (orientation: landscape)',
428
+ portrait : 'only screen and (orientation: portrait)',
429
+ retina : 'only screen and (-webkit-min-device-pixel-ratio: 2),' +
430
+ 'only screen and (min--moz-device-pixel-ratio: 2),' +
431
+ 'only screen and (-o-min-device-pixel-ratio: 2/1),' +
432
+ 'only screen and (min-device-pixel-ratio: 2),' +
433
+ 'only screen and (min-resolution: 192dpi),' +
434
+ 'only screen and (min-resolution: 2dppx)'
435
+ },
436
+ classPrefix = 'foundation-mq-',
437
+ classList = ['small', 'medium', 'large', 'xlarge', 'xxlarge'],
438
+ head = angular.element($document[0].querySelector('head'));
439
+
440
+ for (var i = 0; i < classList.length; i++) {
441
+ head.append('<meta class="' + classPrefix + classList[i] + '" />');
442
+ element = getComputedStyle(head[0].querySelector('meta.' + classPrefix + classList[i]));
443
+ mediaSize = element.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g, '');
444
+ formatList[classList[i]] = mediaSize;
445
+ }
446
+ return formatList;
447
+ }])
448
+
449
+ /**
450
+ * @ngdoc function
451
+ * @name mm.foundation.interchange.interchangeQueriesManager
452
+ * @function interchangeQueriesManager
453
+ * @description
454
+ *
455
+ * interface to add and remove named queries
456
+ * in the interchangeQueries list
457
+ */
458
+ .factory('interchangeQueriesManager', ['interchangeQueries', function (interchangeQueries) {
459
+ return {
460
+ /**
461
+ * @ngdoc method
462
+ * @name interchangeQueriesManager#add
463
+ * @methodOf mm.foundation.interchange.interchangeQueriesManager
464
+ * @description
465
+ *
466
+ * Add a custom media query in the `interchangeQueries`
467
+ * factory. This method does not allow to update an existing
468
+ * media query.
469
+ *
470
+ * @param {string} name MediaQuery name
471
+ * @param {string} media MediaQuery
472
+ * @returns {boolean} True if the insert is a success
473
+ */
474
+ add: function (name, media) {
475
+ if (!name || !media ||
476
+ !angular.isString(name) || !angular.isString(media) ||
477
+ !!interchangeQueries[name]) {
478
+ return false;
479
+ }
480
+ interchangeQueries[name] = media;
481
+ return true;
482
+ }
483
+ };
484
+ }])
485
+
486
+ /**
487
+ * @ngdoc function
488
+ * @name mm.foundation.interchange.interchangeTools
489
+ * @function interchangeTools
490
+ * @description
491
+ *
492
+ * Tools to help with the `interchange` module.
493
+ */
494
+ .factory('interchangeTools', ['$window', 'interchangeQueries', function ($window, namedQueries) {
495
+
496
+ /**
497
+ * @ngdoc method
498
+ * @name interchangeTools#parseAttribute
499
+ * @methodOf mm.foundation.interchange.interchangeTools
500
+ * @description
501
+ *
502
+ * Attribute parser to transform an `interchange` attribute
503
+ * value to an object with media query (name or query) as key,
504
+ * and file to use as value.
505
+ *
506
+ * ```
507
+ * {
508
+ * small: 'bridge-500.jpg',
509
+ * large: 'bridge-1200.jpg'
510
+ * }
511
+ * ```
512
+ *
513
+ * @param {string} value Interchange query string
514
+ * @returns {object} Attribute parsed
515
+ */
516
+ var parseAttribute = function (value) {
517
+ var raw = value.split(/\[(.*?)\]/),
518
+ i = raw.length,
519
+ breaker = /^(.+)\,\ \((.+)\)$/,
520
+ breaked,
521
+ output = {};
522
+
523
+ while (i--) {
524
+ if (raw[i].replace(/[\W\d]+/, '').length > 4) {
525
+ breaked = breaker.exec(raw[i]);
526
+ if (!!breaked && breaked.length === 3) {
527
+ output[breaked[2]] = breaked[1];
528
+ }
529
+ }
530
+ }
531
+ return output;
532
+ };
533
+
534
+ /**
535
+ * @ngdoc method
536
+ * @name interchangeTools#findCurrentMediaFile
537
+ * @methodOf mm.foundation.interchange.interchangeTools
538
+ * @description
539
+ *
540
+ * Find the current item to display from a file list
541
+ * (object returned by `parseAttribute`) and the
542
+ * current page dimensions.
543
+ *
544
+ * ```
545
+ * {
546
+ * small: 'bridge-500.jpg',
547
+ * large: 'bridge-1200.jpg'
548
+ * }
549
+ * ```
550
+ *
551
+ * @param {object} files Parsed version of `interchange` attribute
552
+ * @returns {string} File to display (or `undefined`)
553
+ */
554
+ var findCurrentMediaFile = function (files) {
555
+ var file, media, match;
556
+ for (file in files) {
557
+ media = namedQueries[file] || file;
558
+ match = $window.matchMedia(media);
559
+ if (match.matches) {
560
+ return files[file];
561
+ }
562
+ }
563
+ return;
564
+ };
565
+
566
+ return {
567
+ parseAttribute: parseAttribute,
568
+ findCurrentMediaFile: findCurrentMediaFile
569
+ };
570
+ }])
571
+
572
+ /**
573
+ * @ngdoc directive
574
+ * @name mm.foundation.interchange.directive:interchange
575
+ * @restrict A
576
+ * @element DIV|IMG
577
+ * @priority 450
578
+ * @scope true
579
+ * @description
580
+ *
581
+ * Interchange directive, following the same features as
582
+ * ZURB documentation. The directive is splitted in 3 parts.
583
+ *
584
+ * 1. This directive use `compile` and not `link` for a simple
585
+ * reason: if the method is applied on a DIV element to
586
+ * display a template, the compile method will inject an ng-include.
587
+ * Because using a `templateUrl` or `template` to do it wouldn't
588
+ * be appropriate for all cases (`IMG` or dynamic backgrounds).
589
+ * And doing it in `link` is too late to be applied.
590
+ *
591
+ * 2. In the `compile:post`, the attribute is parsed to find
592
+ * out the type of content to display.
593
+ *
594
+ * 3. At the start and on event `resize`, the method `replace`
595
+ * is called to reevaluate which file is supposed to be displayed
596
+ * and update the value if necessary. The methd will also
597
+ * trigger a `replace` event.
598
+ */
599
+ .directive('interchange', ['$window', '$rootScope', 'interchangeTools', function ($window, $rootScope, interchangeTools) {
600
+
601
+ var pictureFilePattern = /[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i;
602
+
603
+ return {
604
+ restrict: 'A',
605
+ scope: true,
606
+ priority: 450,
607
+ compile: function compile($element, attrs) {
608
+ // Set up the attribute to update
609
+ if ($element[0].nodeName === 'DIV' && !pictureFilePattern.test(attrs.interchange)) {
610
+ $element.html('<ng-include src="currentFile"></ng-include>');
611
+ }
612
+
613
+ return {
614
+ pre: function preLink($scope, $element, attrs) {},
615
+ post: function postLink($scope, $element, attrs) {
616
+ var currentFile, nodeName;
617
+
618
+ // Set up the attribute to update
619
+ nodeName = $element && $element[0] && $element[0].nodeName;
620
+ $scope.fileMap = interchangeTools.parseAttribute(attrs.interchange);
621
+
622
+ // Find the type of interchange
623
+ switch (nodeName) {
624
+ case 'DIV':
625
+ // If the tag is a div, we test the current file to see if it's picture
626
+ currentFile = interchangeTools.findCurrentMediaFile($scope.fileMap);
627
+ if (/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(currentFile)) {
628
+ $scope.type = 'background';
629
+ }
630
+ else {
631
+ $scope.type = 'include';
632
+ }
633
+ break;
634
+
635
+ case 'IMG':
636
+ $scope.type = 'image';
637
+ break;
638
+
639
+ default:
640
+ return;
641
+ }
642
+
643
+ var replace = function (e) {
644
+ // The the new file to display (exit if the same)
645
+ var currentFile = interchangeTools.findCurrentMediaFile($scope.fileMap);
646
+ if (!!$scope.currentFile && $scope.currentFile === currentFile) {
647
+ return;
648
+ }
649
+
650
+ // Set up the new file
651
+ $scope.currentFile = currentFile;
652
+ switch ($scope.type) {
653
+ case 'image':
654
+ $element.attr('src', $scope.currentFile);
655
+ break;
656
+
657
+ case 'background':
658
+ $element.css('background-image', 'url(' + $scope.currentFile + ')');
659
+ break;
660
+ }
661
+
662
+ // Trigger events
663
+ $rootScope.$emit('replace', $element, $scope);
664
+ if (!!e) {
665
+ $scope.$apply();
666
+ }
667
+ };
668
+
669
+ // Start
670
+ replace();
671
+ $window.addEventListener('resize', replace);
672
+ $scope.$on('$destroy', function () {
673
+ $window.removeEventListener('resize', replace);
674
+ });
675
+ }
676
+ };
677
+ }
678
+ };
679
+ }]);
680
+
681
+ angular.module('mm.foundation.transition', [])
682
+
683
+ /**
684
+ * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
685
+ * @param {DOMElement} element The DOMElement that will be animated.
686
+ * @param {string|object|function} trigger The thing that will cause the transition to start:
687
+ * - As a string, it represents the css class to be added to the element.
688
+ * - As an object, it represents a hash of style attributes to be applied to the element.
689
+ * - As a function, it represents a function to be called that will cause the transition to occur.
690
+ * @return {Promise} A promise that is resolved when the transition finishes.
691
+ */
692
+ .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
693
+
694
+ var $transition = function(element, trigger, options) {
695
+ options = options || {};
696
+ var deferred = $q.defer();
697
+ var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
698
+
699
+ var transitionEndHandler = function(event) {
700
+ $rootScope.$apply(function() {
701
+ element.unbind(endEventName, transitionEndHandler);
702
+ deferred.resolve(element);
703
+ });
704
+ };
705
+
706
+ if (endEventName) {
707
+ element.bind(endEventName, transitionEndHandler);
708
+ }
709
+
710
+ // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
711
+ $timeout(function() {
712
+ if ( angular.isString(trigger) ) {
713
+ element.addClass(trigger);
714
+ } else if ( angular.isFunction(trigger) ) {
715
+ trigger(element);
716
+ } else if ( angular.isObject(trigger) ) {
717
+ element.css(trigger);
718
+ }
719
+ //If browser does not support transitions, instantly resolve
720
+ if ( !endEventName ) {
721
+ deferred.resolve(element);
722
+ }
723
+ });
724
+
725
+ // Add our custom cancel function to the promise that is returned
726
+ // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
727
+ // i.e. it will therefore never raise a transitionEnd event for that transition
728
+ deferred.promise.cancel = function() {
729
+ if ( endEventName ) {
730
+ element.unbind(endEventName, transitionEndHandler);
731
+ }
732
+ deferred.reject('Transition cancelled');
733
+ };
734
+
735
+ return deferred.promise;
736
+ };
737
+
738
+ // Work out the name of the transitionEnd event
739
+ var transElement = document.createElement('trans');
740
+ var transitionEndEventNames = {
741
+ 'WebkitTransition': 'webkitTransitionEnd',
742
+ 'MozTransition': 'transitionend',
743
+ 'OTransition': 'oTransitionEnd',
744
+ 'transition': 'transitionend'
745
+ };
746
+ var animationEndEventNames = {
747
+ 'WebkitTransition': 'webkitAnimationEnd',
748
+ 'MozTransition': 'animationend',
749
+ 'OTransition': 'oAnimationEnd',
750
+ 'transition': 'animationend'
751
+ };
752
+ function findEndEventName(endEventNames) {
753
+ for (var name in endEventNames){
754
+ if (transElement.style[name] !== undefined) {
755
+ return endEventNames[name];
756
+ }
757
+ }
758
+ }
759
+ $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
760
+ $transition.animationEndEventName = findEndEventName(animationEndEventNames);
761
+ return $transition;
762
+ }]);
763
+
764
+ angular.module('mm.foundation.modal', ['mm.foundation.transition'])
765
+
766
+ /**
767
+ * A helper, internal data structure that acts as a map but also allows getting / removing
768
+ * elements in the LIFO order
769
+ */
770
+ .factory('$$stackedMap', function () {
771
+ return {
772
+ createNew: function () {
773
+ var stack = [];
774
+
775
+ return {
776
+ add: function (key, value) {
777
+ stack.push({
778
+ key: key,
779
+ value: value
780
+ });
781
+ },
782
+ get: function (key) {
783
+ for (var i = 0; i < stack.length; i++) {
784
+ if (key == stack[i].key) {
785
+ return stack[i];
786
+ }
787
+ }
788
+ },
789
+ keys: function() {
790
+ var keys = [];
791
+ for (var i = 0; i < stack.length; i++) {
792
+ keys.push(stack[i].key);
793
+ }
794
+ return keys;
795
+ },
796
+ top: function () {
797
+ return stack[stack.length - 1];
798
+ },
799
+ remove: function (key) {
800
+ var idx = -1;
801
+ for (var i = 0; i < stack.length; i++) {
802
+ if (key == stack[i].key) {
803
+ idx = i;
804
+ break;
805
+ }
806
+ }
807
+ return stack.splice(idx, 1)[0];
808
+ },
809
+ removeTop: function () {
810
+ return stack.splice(stack.length - 1, 1)[0];
811
+ },
812
+ length: function () {
813
+ return stack.length;
814
+ }
815
+ };
816
+ }
817
+ };
818
+ })
819
+
820
+ /**
821
+ * A helper directive for the $modal service. It creates a backdrop element.
822
+ */
823
+ .directive('modalBackdrop', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
824
+ return {
825
+ restrict: 'EA',
826
+ replace: true,
827
+ templateUrl: 'template/modal/backdrop.html',
828
+ link: function (scope) {
829
+
830
+ scope.animate = false;
831
+
832
+ //trigger CSS transitions
833
+ $timeout(function () {
834
+ scope.animate = true;
835
+ });
836
+
837
+ scope.close = function (evt) {
838
+ var modal = $modalStack.getTop();
839
+ if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
840
+ evt.preventDefault();
841
+ evt.stopPropagation();
842
+ $modalStack.dismiss(modal.key, 'backdrop click');
843
+ }
844
+ };
845
+ }
846
+ };
847
+ }])
848
+
849
+ .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
850
+ return {
851
+ restrict: 'EA',
852
+ scope: {
853
+ index: '@',
854
+ animate: '='
855
+ },
856
+ replace: true,
857
+ transclude: true,
858
+ templateUrl: 'template/modal/window.html',
859
+ link: function (scope, element, attrs) {
860
+ scope.windowClass = attrs.windowClass || '';
861
+
862
+ $timeout(function () {
863
+ // trigger CSS transitions
864
+ scope.animate = true;
865
+
866
+ // If the modal contains any autofocus elements refocus onto the first one
867
+ if (element[0].querySelectorAll('[autofocus]').length > 0) {
868
+ element[0].querySelectorAll('[autofocus]')[0].focus();
869
+ }
870
+ else{
871
+ // otherwise focus the freshly-opened modal
872
+ element[0].focus();
873
+ }
874
+ });
875
+ }
876
+ };
877
+ }])
878
+
879
+ .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
880
+ function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
881
+
882
+ var OPENED_MODAL_CLASS = 'modal-open';
883
+
884
+ var backdropDomEl, backdropScope;
885
+ var openedWindows = $$stackedMap.createNew();
886
+ var $modalStack = {};
887
+
888
+ function backdropIndex() {
889
+ var topBackdropIndex = -1;
890
+ var opened = openedWindows.keys();
891
+ for (var i = 0; i < opened.length; i++) {
892
+ if (openedWindows.get(opened[i]).value.backdrop) {
893
+ topBackdropIndex = i;
894
+ }
895
+ }
896
+ return topBackdropIndex;
897
+ }
898
+
899
+ $rootScope.$watch(backdropIndex, function(newBackdropIndex){
900
+ if (backdropScope) {
901
+ backdropScope.index = newBackdropIndex;
902
+ }
903
+ });
904
+
905
+ function removeModalWindow(modalInstance) {
906
+
907
+ var body = $document.find('body').eq(0);
908
+ var modalWindow = openedWindows.get(modalInstance).value;
909
+
910
+ //clean up the stack
911
+ openedWindows.remove(modalInstance);
912
+
913
+ //remove window DOM element
914
+ removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, checkRemoveBackdrop);
915
+ body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
916
+ }
917
+
918
+ function checkRemoveBackdrop() {
919
+ //remove backdrop if no longer needed
920
+ if (backdropDomEl && backdropIndex() == -1) {
921
+ var backdropScopeRef = backdropScope;
922
+ removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
923
+ backdropScopeRef.$destroy();
924
+ backdropScopeRef = null;
925
+ });
926
+ backdropDomEl = undefined;
927
+ backdropScope = undefined;
928
+ }
929
+ }
930
+
931
+ function removeAfterAnimate(domEl, scope, emulateTime, done) {
932
+ // Closing animation
933
+ scope.animate = false;
934
+
935
+ var transitionEndEventName = $transition.transitionEndEventName;
936
+ if (transitionEndEventName) {
937
+ // transition out
938
+ var timeout = $timeout(afterAnimating, emulateTime);
939
+
940
+ domEl.bind(transitionEndEventName, function () {
941
+ $timeout.cancel(timeout);
942
+ afterAnimating();
943
+ scope.$apply();
944
+ });
945
+ } else {
946
+ // Ensure this call is async
947
+ $timeout(afterAnimating, 0);
948
+ }
949
+
950
+ function afterAnimating() {
951
+ if (afterAnimating.done) {
952
+ return;
953
+ }
954
+ afterAnimating.done = true;
955
+
956
+ domEl.remove();
957
+ if (done) {
958
+ done();
959
+ }
960
+ }
961
+ }
962
+
963
+ $document.bind('keydown', function (evt) {
964
+ var modal;
965
+
966
+ if (evt.which === 27) {
967
+ modal = openedWindows.top();
968
+ if (modal && modal.value.keyboard) {
969
+ $rootScope.$apply(function () {
970
+ $modalStack.dismiss(modal.key);
971
+ });
972
+ }
973
+ }
974
+ });
975
+
976
+ $modalStack.open = function (modalInstance, modal) {
977
+
978
+ openedWindows.add(modalInstance, {
979
+ deferred: modal.deferred,
980
+ modalScope: modal.scope,
981
+ backdrop: modal.backdrop,
982
+ keyboard: modal.keyboard
983
+ });
984
+
985
+ var body = $document.find('body').eq(0),
986
+ currBackdropIndex = backdropIndex();
987
+
988
+ if (currBackdropIndex >= 0 && !backdropDomEl) {
989
+ backdropScope = $rootScope.$new(true);
990
+ backdropScope.index = currBackdropIndex;
991
+ backdropDomEl = $compile('<div modal-backdrop></div>')(backdropScope);
992
+ body.append(backdropDomEl);
993
+ }
994
+
995
+ var angularDomEl = angular.element('<div modal-window></div>');
996
+ angularDomEl.attr('window-class', modal.windowClass);
997
+ angularDomEl.attr('index', openedWindows.length() - 1);
998
+ angularDomEl.attr('animate', 'animate');
999
+ angularDomEl.html(modal.content);
1000
+
1001
+ var modalDomEl = $compile(angularDomEl)(modal.scope);
1002
+ openedWindows.top().value.modalDomEl = modalDomEl;
1003
+ body.append(modalDomEl);
1004
+ body.addClass(OPENED_MODAL_CLASS);
1005
+ };
1006
+
1007
+ $modalStack.close = function (modalInstance, result) {
1008
+ var modalWindow = openedWindows.get(modalInstance).value;
1009
+ if (modalWindow) {
1010
+ modalWindow.deferred.resolve(result);
1011
+ removeModalWindow(modalInstance);
1012
+ }
1013
+ };
1014
+
1015
+ $modalStack.dismiss = function (modalInstance, reason) {
1016
+ var modalWindow = openedWindows.get(modalInstance).value;
1017
+ if (modalWindow) {
1018
+ modalWindow.deferred.reject(reason);
1019
+ removeModalWindow(modalInstance);
1020
+ }
1021
+ };
1022
+
1023
+ $modalStack.dismissAll = function (reason) {
1024
+ var topModal = this.getTop();
1025
+ while (topModal) {
1026
+ this.dismiss(topModal.key, reason);
1027
+ topModal = this.getTop();
1028
+ }
1029
+ };
1030
+
1031
+ $modalStack.getTop = function () {
1032
+ return openedWindows.top();
1033
+ };
1034
+
1035
+ return $modalStack;
1036
+ }])
1037
+
1038
+ .provider('$modal', function () {
1039
+
1040
+ var $modalProvider = {
1041
+ options: {
1042
+ backdrop: true, //can be also false or 'static'
1043
+ keyboard: true
1044
+ },
1045
+ $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
1046
+ function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
1047
+
1048
+ var $modal = {};
1049
+
1050
+ function getTemplatePromise(options) {
1051
+ return options.template ? $q.when(options.template) :
1052
+ $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
1053
+ return result.data;
1054
+ });
1055
+ }
1056
+
1057
+ function getResolvePromises(resolves) {
1058
+ var promisesArr = [];
1059
+ angular.forEach(resolves, function (value, key) {
1060
+ if (angular.isFunction(value) || angular.isArray(value)) {
1061
+ promisesArr.push($q.when($injector.invoke(value)));
1062
+ }
1063
+ });
1064
+ return promisesArr;
1065
+ }
1066
+
1067
+ $modal.open = function (modalOptions) {
1068
+
1069
+ var modalResultDeferred = $q.defer();
1070
+ var modalOpenedDeferred = $q.defer();
1071
+
1072
+ //prepare an instance of a modal to be injected into controllers and returned to a caller
1073
+ var modalInstance = {
1074
+ result: modalResultDeferred.promise,
1075
+ opened: modalOpenedDeferred.promise,
1076
+ close: function (result) {
1077
+ $modalStack.close(modalInstance, result);
1078
+ },
1079
+ dismiss: function (reason) {
1080
+ $modalStack.dismiss(modalInstance, reason);
1081
+ }
1082
+ };
1083
+
1084
+ //merge and clean up options
1085
+ modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
1086
+ modalOptions.resolve = modalOptions.resolve || {};
1087
+
1088
+ //verify options
1089
+ if (!modalOptions.template && !modalOptions.templateUrl) {
1090
+ throw new Error('One of template or templateUrl options is required.');
1091
+ }
1092
+
1093
+ var templateAndResolvePromise =
1094
+ $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
1095
+
1096
+
1097
+ templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
1098
+
1099
+ var modalScope = (modalOptions.scope || $rootScope).$new();
1100
+ modalScope.$close = modalInstance.close;
1101
+ modalScope.$dismiss = modalInstance.dismiss;
1102
+
1103
+ var ctrlInstance, ctrlLocals = {};
1104
+ var resolveIter = 1;
1105
+
1106
+ //controllers
1107
+ if (modalOptions.controller) {
1108
+ ctrlLocals.$scope = modalScope;
1109
+ ctrlLocals.$modalInstance = modalInstance;
1110
+ angular.forEach(modalOptions.resolve, function (value, key) {
1111
+ ctrlLocals[key] = tplAndVars[resolveIter++];
1112
+ });
1113
+
1114
+ ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
1115
+ }
1116
+
1117
+ $modalStack.open(modalInstance, {
1118
+ scope: modalScope,
1119
+ deferred: modalResultDeferred,
1120
+ content: tplAndVars[0],
1121
+ backdrop: modalOptions.backdrop,
1122
+ keyboard: modalOptions.keyboard,
1123
+ windowClass: modalOptions.windowClass
1124
+ });
1125
+
1126
+ }, function resolveError(reason) {
1127
+ modalResultDeferred.reject(reason);
1128
+ });
1129
+
1130
+ templateAndResolvePromise.then(function () {
1131
+ modalOpenedDeferred.resolve(true);
1132
+ }, function () {
1133
+ modalOpenedDeferred.reject(false);
1134
+ });
1135
+
1136
+ return modalInstance;
1137
+ };
1138
+
1139
+ return $modal;
1140
+ }]
1141
+ };
1142
+
1143
+ return $modalProvider;
1144
+ });
1145
+
1146
+ angular.module("mm.foundation.offcanvas", [])
1147
+ .directive('offCanvasWrap', ['$window', function ($window) {
1148
+ return {
1149
+ scope: {},
1150
+ restrict: 'C',
1151
+ link: function ($scope, element, attrs) {
1152
+ var win = angular.element($window);
1153
+ var sidebar = $scope.sidebar = element;
1154
+
1155
+ $scope.hide = function () {
1156
+ sidebar.removeClass('move-left');
1157
+ sidebar.removeClass('move-right');
1158
+ };
1159
+
1160
+ win.bind("resize.body", $scope.hide);
1161
+
1162
+ $scope.$on('$destroy', function() {
1163
+ win.unbind("resize.body", $scope.hide);
1164
+ });
1165
+
1166
+ },
1167
+ controller: ['$scope', function($scope) {
1168
+
1169
+ this.leftToggle = function() {
1170
+ $scope.sidebar.toggleClass("move-right");
1171
+ };
1172
+
1173
+ this.rightToggle = function() {
1174
+ $scope.sidebar.toggleClass("move-left");
1175
+ };
1176
+
1177
+ this.hide = function() {
1178
+ $scope.hide();
1179
+ };
1180
+ }]
1181
+ };
1182
+ }])
1183
+ .directive('leftOffCanvasToggle', [function () {
1184
+ return {
1185
+ require: '^offCanvasWrap',
1186
+ restrict: 'C',
1187
+ link: function ($scope, element, attrs, offCanvasWrap) {
1188
+ element.on('click', function () {
1189
+ offCanvasWrap.leftToggle();
1190
+ });
1191
+ }
1192
+ };
1193
+ }])
1194
+ .directive('rightOffCanvasToggle', [function () {
1195
+ return {
1196
+ require: '^offCanvasWrap',
1197
+ restrict: 'C',
1198
+ link: function ($scope, element, attrs, offCanvasWrap) {
1199
+ element.on('click', function () {
1200
+ offCanvasWrap.rightToggle();
1201
+ });
1202
+ }
1203
+ };
1204
+ }])
1205
+ .directive('exitOffCanvas', [function () {
1206
+ return {
1207
+ require: '^offCanvasWrap',
1208
+ restrict: 'C',
1209
+ link: function ($scope, element, attrs, offCanvasWrap) {
1210
+ element.on('click', function () {
1211
+ offCanvasWrap.hide();
1212
+ });
1213
+ }
1214
+ };
1215
+ }])
1216
+ .directive('offCanvasList', [function () {
1217
+ return {
1218
+ require: '^offCanvasWrap',
1219
+ restrict: 'C',
1220
+ link: function ($scope, element, attrs, offCanvasWrap) {
1221
+ element.on('click', function () {
1222
+ offCanvasWrap.hide();
1223
+ });
1224
+ }
1225
+ };
1226
+ }]);
1227
+
1228
+ angular.module('mm.foundation.pagination', [])
1229
+
1230
+ .controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) {
1231
+ var self = this,
1232
+ setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
1233
+
1234
+ this.init = function(defaultItemsPerPage) {
1235
+ if ($attrs.itemsPerPage) {
1236
+ $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
1237
+ self.itemsPerPage = parseInt(value, 10);
1238
+ $scope.totalPages = self.calculateTotalPages();
1239
+ });
1240
+ } else {
1241
+ this.itemsPerPage = defaultItemsPerPage;
1242
+ }
1243
+ };
1244
+
1245
+ this.noPrevious = function() {
1246
+ return this.page === 1;
1247
+ };
1248
+ this.noNext = function() {
1249
+ return this.page === $scope.totalPages;
1250
+ };
1251
+
1252
+ this.isActive = function(page) {
1253
+ return this.page === page;
1254
+ };
1255
+
1256
+ this.calculateTotalPages = function() {
1257
+ var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
1258
+ return Math.max(totalPages || 0, 1);
1259
+ };
1260
+
1261
+ this.getAttributeValue = function(attribute, defaultValue, interpolate) {
1262
+ return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue;
1263
+ };
1264
+
1265
+ this.render = function() {
1266
+ this.page = parseInt($scope.page, 10) || 1;
1267
+ if (this.page > 0 && this.page <= $scope.totalPages) {
1268
+ $scope.pages = this.getPages(this.page, $scope.totalPages);
1269
+ }
1270
+ };
1271
+
1272
+ $scope.selectPage = function(page) {
1273
+ if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) {
1274
+ $scope.page = page;
1275
+ $scope.onSelectPage({ page: page });
1276
+ }
1277
+ };
1278
+
1279
+ $scope.$watch('page', function() {
1280
+ self.render();
1281
+ });
1282
+
1283
+ $scope.$watch('totalItems', function() {
1284
+ $scope.totalPages = self.calculateTotalPages();
1285
+ });
1286
+
1287
+ $scope.$watch('totalPages', function(value) {
1288
+ setNumPages($scope.$parent, value); // Readonly variable
1289
+
1290
+ if ( self.page > value ) {
1291
+ $scope.selectPage(value);
1292
+ } else {
1293
+ self.render();
1294
+ }
1295
+ });
1296
+ }])
1297
+
1298
+ .constant('paginationConfig', {
1299
+ itemsPerPage: 10,
1300
+ boundaryLinks: false,
1301
+ directionLinks: true,
1302
+ firstText: 'First',
1303
+ previousText: 'Previous',
1304
+ nextText: 'Next',
1305
+ lastText: 'Last',
1306
+ rotate: true
1307
+ })
1308
+
1309
+ .directive('pagination', ['$parse', 'paginationConfig', function($parse, config) {
1310
+ return {
1311
+ restrict: 'EA',
1312
+ scope: {
1313
+ page: '=',
1314
+ totalItems: '=',
1315
+ onSelectPage:' &'
1316
+ },
1317
+ controller: 'PaginationController',
1318
+ templateUrl: 'template/pagination/pagination.html',
1319
+ replace: true,
1320
+ link: function(scope, element, attrs, paginationCtrl) {
1321
+
1322
+ // Setup configuration parameters
1323
+ var maxSize,
1324
+ boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ),
1325
+ directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ),
1326
+ firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true),
1327
+ previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
1328
+ nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
1329
+ lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true),
1330
+ rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate);
1331
+
1332
+ paginationCtrl.init(config.itemsPerPage);
1333
+
1334
+ if (attrs.maxSize) {
1335
+ scope.$parent.$watch($parse(attrs.maxSize), function(value) {
1336
+ maxSize = parseInt(value, 10);
1337
+ paginationCtrl.render();
1338
+ });
1339
+ }
1340
+
1341
+ // Create page object used in template
1342
+ function makePage(number, text, isActive, isDisabled) {
1343
+ return {
1344
+ number: number,
1345
+ text: text,
1346
+ active: isActive,
1347
+ disabled: isDisabled
1348
+ };
1349
+ }
1350
+
1351
+ paginationCtrl.getPages = function(currentPage, totalPages) {
1352
+ var pages = [];
1353
+
1354
+ // Default page limits
1355
+ var startPage = 1, endPage = totalPages;
1356
+ var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );
1357
+
1358
+ // recompute if maxSize
1359
+ if ( isMaxSized ) {
1360
+ if ( rotate ) {
1361
+ // Current page is displayed in the middle of the visible ones
1362
+ startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
1363
+ endPage = startPage + maxSize - 1;
1364
+
1365
+ // Adjust if limit is exceeded
1366
+ if (endPage > totalPages) {
1367
+ endPage = totalPages;
1368
+ startPage = endPage - maxSize + 1;
1369
+ }
1370
+ } else {
1371
+ // Visible pages are paginated with maxSize
1372
+ startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
1373
+
1374
+ // Adjust last page if limit is exceeded
1375
+ endPage = Math.min(startPage + maxSize - 1, totalPages);
1376
+ }
1377
+ }
1378
+
1379
+ // Add page number links
1380
+ for (var number = startPage; number <= endPage; number++) {
1381
+ var page = makePage(number, number, paginationCtrl.isActive(number), false);
1382
+ pages.push(page);
1383
+ }
1384
+
1385
+ // Add links to move between page sets
1386
+ if ( isMaxSized && ! rotate ) {
1387
+ if ( startPage > 1 ) {
1388
+ var previousPageSet = makePage(startPage - 1, '...', false, false);
1389
+ pages.unshift(previousPageSet);
1390
+ }
1391
+
1392
+ if ( endPage < totalPages ) {
1393
+ var nextPageSet = makePage(endPage + 1, '...', false, false);
1394
+ pages.push(nextPageSet);
1395
+ }
1396
+ }
1397
+
1398
+ // Add previous & next links
1399
+ if (directionLinks) {
1400
+ var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious());
1401
+ pages.unshift(previousPage);
1402
+
1403
+ var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext());
1404
+ pages.push(nextPage);
1405
+ }
1406
+
1407
+ // Add first & last links
1408
+ if (boundaryLinks) {
1409
+ var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious());
1410
+ pages.unshift(firstPage);
1411
+
1412
+ var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext());
1413
+ pages.push(lastPage);
1414
+ }
1415
+
1416
+ return pages;
1417
+ };
1418
+ }
1419
+ };
1420
+ }])
1421
+
1422
+ .constant('pagerConfig', {
1423
+ itemsPerPage: 10,
1424
+ previousText: '« Previous',
1425
+ nextText: 'Next »',
1426
+ align: true
1427
+ })
1428
+
1429
+ .directive('pager', ['pagerConfig', function(config) {
1430
+ return {
1431
+ restrict: 'EA',
1432
+ scope: {
1433
+ page: '=',
1434
+ totalItems: '=',
1435
+ onSelectPage:' &'
1436
+ },
1437
+ controller: 'PaginationController',
1438
+ templateUrl: 'template/pagination/pager.html',
1439
+ replace: true,
1440
+ link: function(scope, element, attrs, paginationCtrl) {
1441
+
1442
+ // Setup configuration parameters
1443
+ var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
1444
+ nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
1445
+ align = paginationCtrl.getAttributeValue(attrs.align, config.align);
1446
+
1447
+ paginationCtrl.init(config.itemsPerPage);
1448
+
1449
+ // Create page object used in template
1450
+ function makePage(number, text, isDisabled, isPrevious, isNext) {
1451
+ return {
1452
+ number: number,
1453
+ text: text,
1454
+ disabled: isDisabled,
1455
+ previous: ( align && isPrevious ),
1456
+ next: ( align && isNext )
1457
+ };
1458
+ }
1459
+
1460
+ paginationCtrl.getPages = function(currentPage) {
1461
+ return [
1462
+ makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false),
1463
+ makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true)
1464
+ ];
1465
+ };
1466
+ }
1467
+ };
1468
+ }]);
1469
+
1470
+ /**
1471
+ * The following features are still outstanding: animation as a
1472
+ * function, placement as a function, inside, support for more triggers than
1473
+ * just mouse enter/leave, html tooltips, and selector delegation.
1474
+ */
1475
+ angular.module( 'mm.foundation.tooltip', [ 'mm.foundation.position', 'mm.foundation.bindHtml' ] )
1476
+
1477
+ /**
1478
+ * The $tooltip service creates tooltip- and popover-like directives as well as
1479
+ * houses global options for them.
1480
+ */
1481
+ .provider( '$tooltip', function () {
1482
+ // The default options tooltip and popover.
1483
+ var defaultOptions = {
1484
+ placement: 'top',
1485
+ animation: true,
1486
+ popupDelay: 0
1487
+ };
1488
+
1489
+ // Default hide triggers for each show trigger
1490
+ var triggerMap = {
1491
+ 'mouseenter': 'mouseleave',
1492
+ 'click': 'click',
1493
+ 'focus': 'blur'
1494
+ };
1495
+
1496
+ // The options specified to the provider globally.
1497
+ var globalOptions = {};
1498
+
1499
+ /**
1500
+ * `options({})` allows global configuration of all tooltips in the
1501
+ * application.
1502
+ *
1503
+ * var app = angular.module( 'App', ['mm.foundation.tooltip'], function( $tooltipProvider ) {
1504
+ * // place tooltips left instead of top by default
1505
+ * $tooltipProvider.options( { placement: 'left' } );
1506
+ * });
1507
+ */
1508
+ this.options = function( value ) {
1509
+ angular.extend( globalOptions, value );
1510
+ };
1511
+
1512
+ /**
1513
+ * This allows you to extend the set of trigger mappings available. E.g.:
1514
+ *
1515
+ * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
1516
+ */
1517
+ this.setTriggers = function setTriggers ( triggers ) {
1518
+ angular.extend( triggerMap, triggers );
1519
+ };
1520
+
1521
+ /**
1522
+ * This is a helper function for translating camel-case to snake-case.
1523
+ */
1524
+ function snake_case(name){
1525
+ var regexp = /[A-Z]/g;
1526
+ var separator = '-';
1527
+ return name.replace(regexp, function(letter, pos) {
1528
+ return (pos ? separator : '') + letter.toLowerCase();
1529
+ });
1530
+ }
1531
+
1532
+ /**
1533
+ * Returns the actual instance of the $tooltip service.
1534
+ * TODO support multiple triggers
1535
+ */
1536
+ this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) {
1537
+ return function $tooltip ( type, prefix, defaultTriggerShow ) {
1538
+ var options = angular.extend( {}, defaultOptions, globalOptions );
1539
+
1540
+ /**
1541
+ * Returns an object of show and hide triggers.
1542
+ *
1543
+ * If a trigger is supplied,
1544
+ * it is used to show the tooltip; otherwise, it will use the `trigger`
1545
+ * option passed to the `$tooltipProvider.options` method; else it will
1546
+ * default to the trigger supplied to this directive factory.
1547
+ *
1548
+ * The hide trigger is based on the show trigger. If the `trigger` option
1549
+ * was passed to the `$tooltipProvider.options` method, it will use the
1550
+ * mapped trigger from `triggerMap` or the passed trigger if the map is
1551
+ * undefined; otherwise, it uses the `triggerMap` value of the show
1552
+ * trigger; else it will just use the show trigger.
1553
+ */
1554
+ function getTriggers ( trigger ) {
1555
+ var show = trigger || options.trigger || defaultTriggerShow;
1556
+ var hide = triggerMap[show] || show;
1557
+ return {
1558
+ show: show,
1559
+ hide: hide
1560
+ };
1561
+ }
1562
+
1563
+ var directiveName = snake_case( type );
1564
+
1565
+ var startSym = $interpolate.startSymbol();
1566
+ var endSym = $interpolate.endSymbol();
1567
+ var template =
1568
+ '<div '+ directiveName +'-popup '+
1569
+ 'title="'+startSym+'tt_title'+endSym+'" '+
1570
+ 'content="'+startSym+'tt_content'+endSym+'" '+
1571
+ 'placement="'+startSym+'tt_placement'+endSym+'" '+
1572
+ 'animation="tt_animation" '+
1573
+ 'is-open="tt_isOpen"'+
1574
+ '>'+
1575
+ '</div>';
1576
+
1577
+ return {
1578
+ restrict: 'EA',
1579
+ scope: true,
1580
+ compile: function (tElem, tAttrs) {
1581
+ var tooltipLinker = $compile( template );
1582
+
1583
+ return function link ( scope, element, attrs ) {
1584
+ var tooltip;
1585
+ var transitionTimeout;
1586
+ var popupTimeout;
1587
+ var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
1588
+ var triggers = getTriggers( undefined );
1589
+ var hasRegisteredTriggers = false;
1590
+ var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
1591
+
1592
+ var positionTooltip = function (){
1593
+ var position,
1594
+ ttWidth,
1595
+ ttHeight,
1596
+ ttPosition;
1597
+ // Get the position of the directive element.
1598
+ position = appendToBody ? $position.offset( element ) : $position.position( element );
1599
+
1600
+ // Get the height and width of the tooltip so we can center it.
1601
+ ttWidth = tooltip.prop( 'offsetWidth' );
1602
+ ttHeight = tooltip.prop( 'offsetHeight' );
1603
+
1604
+ // Calculate the tooltip's top and left coordinates to center it with
1605
+ // this directive.
1606
+ switch ( scope.tt_placement ) {
1607
+ case 'right':
1608
+ ttPosition = {
1609
+ top: position.top + position.height / 2 - ttHeight / 2,
1610
+ left: position.left + position.width + 10
1611
+ };
1612
+ break;
1613
+ case 'bottom':
1614
+ ttPosition = {
1615
+ top: position.top + position.height + 10,
1616
+ left: position.left
1617
+ };
1618
+ break;
1619
+ case 'left':
1620
+ ttPosition = {
1621
+ top: position.top + position.height / 2 - ttHeight / 2,
1622
+ left: position.left - ttWidth - 10
1623
+ };
1624
+ break;
1625
+ default:
1626
+ ttPosition = {
1627
+ top: position.top - ttHeight - 10,
1628
+ left: position.left
1629
+ };
1630
+ break;
1631
+ }
1632
+
1633
+ ttPosition.top += 'px';
1634
+ ttPosition.left += 'px';
1635
+
1636
+ // Now set the calculated positioning.
1637
+ tooltip.css( ttPosition );
1638
+
1639
+ };
1640
+
1641
+ // By default, the tooltip is not open.
1642
+ // TODO add ability to start tooltip opened
1643
+ scope.tt_isOpen = false;
1644
+
1645
+ function toggleTooltipBind () {
1646
+ if ( ! scope.tt_isOpen ) {
1647
+ showTooltipBind();
1648
+ } else {
1649
+ hideTooltipBind();
1650
+ }
1651
+ }
1652
+
1653
+ // Show the tooltip with delay if specified, otherwise show it immediately
1654
+ function showTooltipBind() {
1655
+ if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) {
1656
+ return;
1657
+ }
1658
+ if ( scope.tt_popupDelay ) {
1659
+ popupTimeout = $timeout( show, scope.tt_popupDelay, false );
1660
+ popupTimeout.then(function(reposition){reposition();});
1661
+ } else {
1662
+ show()();
1663
+ }
1664
+ }
1665
+
1666
+ function hideTooltipBind () {
1667
+ scope.$apply(function () {
1668
+ hide();
1669
+ });
1670
+ }
1671
+
1672
+ // Show the tooltip popup element.
1673
+ function show() {
1674
+
1675
+
1676
+ // Don't show empty tooltips.
1677
+ if ( ! scope.tt_content ) {
1678
+ return angular.noop;
1679
+ }
1680
+
1681
+ createTooltip();
1682
+
1683
+ // If there is a pending remove transition, we must cancel it, lest the
1684
+ // tooltip be mysteriously removed.
1685
+ if ( transitionTimeout ) {
1686
+ $timeout.cancel( transitionTimeout );
1687
+ }
1688
+
1689
+ // Set the initial positioning.
1690
+ tooltip.css({ top: 0, left: 0, display: 'block' });
1691
+
1692
+ // Now we add it to the DOM because need some info about it. But it's not
1693
+ // visible yet anyway.
1694
+ if ( appendToBody ) {
1695
+ $document.find( 'body' ).append( tooltip );
1696
+ } else {
1697
+ element.after( tooltip );
1698
+ }
1699
+
1700
+ positionTooltip();
1701
+
1702
+ // And show the tooltip.
1703
+ scope.tt_isOpen = true;
1704
+ scope.$digest(); // digest required as $apply is not called
1705
+
1706
+ // Return positioning function as promise callback for correct
1707
+ // positioning after draw.
1708
+ return positionTooltip;
1709
+ }
1710
+
1711
+ // Hide the tooltip popup element.
1712
+ function hide() {
1713
+ // First things first: we don't show it anymore.
1714
+ scope.tt_isOpen = false;
1715
+
1716
+ //if tooltip is going to be shown after delay, we must cancel this
1717
+ $timeout.cancel( popupTimeout );
1718
+
1719
+ // And now we remove it from the DOM. However, if we have animation, we
1720
+ // need to wait for it to expire beforehand.
1721
+ // FIXME: this is a placeholder for a port of the transitions library.
1722
+ if ( scope.tt_animation ) {
1723
+ transitionTimeout = $timeout(removeTooltip, 500);
1724
+ } else {
1725
+ removeTooltip();
1726
+ }
1727
+ }
1728
+
1729
+ function createTooltip() {
1730
+ // There can only be one tooltip element per directive shown at once.
1731
+ if (tooltip) {
1732
+ removeTooltip();
1733
+ }
1734
+ tooltip = tooltipLinker(scope, function () {});
1735
+
1736
+ // Get contents rendered into the tooltip
1737
+ scope.$digest();
1738
+ }
1739
+
1740
+ function removeTooltip() {
1741
+ if (tooltip) {
1742
+ tooltip.remove();
1743
+ tooltip = null;
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * Observe the relevant attributes.
1749
+ */
1750
+ attrs.$observe( type, function ( val ) {
1751
+ scope.tt_content = val;
1752
+
1753
+ if (!val && scope.tt_isOpen ) {
1754
+ hide();
1755
+ }
1756
+ });
1757
+
1758
+ attrs.$observe( prefix+'Title', function ( val ) {
1759
+ scope.tt_title = val;
1760
+ });
1761
+
1762
+ attrs.$observe( prefix+'Placement', function ( val ) {
1763
+ scope.tt_placement = angular.isDefined( val ) ? val : options.placement;
1764
+ });
1765
+
1766
+ attrs.$observe( prefix+'PopupDelay', function ( val ) {
1767
+ var delay = parseInt( val, 10 );
1768
+ scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
1769
+ });
1770
+
1771
+ var unregisterTriggers = function() {
1772
+ if (hasRegisteredTriggers) {
1773
+ element.unbind( triggers.show, showTooltipBind );
1774
+ element.unbind( triggers.hide, hideTooltipBind );
1775
+ }
1776
+ };
1777
+
1778
+ var unregisterTriggerFunction = function () {};
1779
+
1780
+ attrs.$observe( prefix+'Trigger', function ( val ) {
1781
+ unregisterTriggers();
1782
+ unregisterTriggerFunction();
1783
+
1784
+ triggers = getTriggers( val );
1785
+
1786
+ if ( angular.isFunction( triggers.show ) ) {
1787
+ unregisterTriggerFunction = scope.$watch( function () {
1788
+ return triggers.show( scope, element, attrs );
1789
+ }, function ( val ) {
1790
+ return val ? $timeout( show ) : $timeout( hide );
1791
+ });
1792
+ } else {
1793
+ if ( triggers.show === triggers.hide ) {
1794
+ element.bind( triggers.show, toggleTooltipBind );
1795
+ } else {
1796
+ element.bind( triggers.show, showTooltipBind );
1797
+ element.bind( triggers.hide, hideTooltipBind );
1798
+ }
1799
+ }
1800
+
1801
+ hasRegisteredTriggers = true;
1802
+ });
1803
+
1804
+ var animation = scope.$eval(attrs[prefix + 'Animation']);
1805
+ scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation;
1806
+
1807
+ attrs.$observe( prefix+'AppendToBody', function ( val ) {
1808
+ appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody;
1809
+ });
1810
+
1811
+ // if a tooltip is attached to <body> we need to remove it on
1812
+ // location change as its parent scope will probably not be destroyed
1813
+ // by the change.
1814
+ if ( appendToBody ) {
1815
+ scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
1816
+ if ( scope.tt_isOpen ) {
1817
+ hide();
1818
+ }
1819
+ });
1820
+ }
1821
+
1822
+ // Make sure tooltip is destroyed and removed.
1823
+ scope.$on('$destroy', function onDestroyTooltip() {
1824
+ $timeout.cancel( transitionTimeout );
1825
+ $timeout.cancel( popupTimeout );
1826
+ unregisterTriggers();
1827
+ unregisterTriggerFunction();
1828
+ removeTooltip();
1829
+ });
1830
+ };
1831
+ }
1832
+ };
1833
+ };
1834
+ }];
1835
+ })
1836
+
1837
+ .directive( 'tooltipPopup', function () {
1838
+ return {
1839
+ restrict: 'EA',
1840
+ replace: true,
1841
+ scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
1842
+ templateUrl: 'template/tooltip/tooltip-popup.html'
1843
+ };
1844
+ })
1845
+
1846
+ .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
1847
+ return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
1848
+ }])
1849
+
1850
+ .directive( 'tooltipHtmlUnsafePopup', function () {
1851
+ return {
1852
+ restrict: 'EA',
1853
+ replace: true,
1854
+ scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
1855
+ templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
1856
+ };
1857
+ })
1858
+
1859
+ .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
1860
+ return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
1861
+ }]);
1862
+
1863
+ /**
1864
+ * The following features are still outstanding: popup delay, animation as a
1865
+ * function, placement as a function, inside, support for more triggers than
1866
+ * just mouse enter/leave, html popovers, and selector delegatation.
1867
+ */
1868
+ angular.module( 'mm.foundation.popover', [ 'mm.foundation.tooltip' ] )
1869
+
1870
+ .directive( 'popoverPopup', function () {
1871
+ return {
1872
+ restrict: 'EA',
1873
+ replace: true,
1874
+ scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
1875
+ templateUrl: 'template/popover/popover.html'
1876
+ };
1877
+ })
1878
+
1879
+ .directive( 'popover', [ '$tooltip', function ( $tooltip ) {
1880
+ return $tooltip( 'popover', 'popover', 'click' );
1881
+ }]);
1882
+
1883
+ angular.module('mm.foundation.progressbar', ['mm.foundation.transition'])
1884
+
1885
+ .constant('progressConfig', {
1886
+ animate: true,
1887
+ max: 100
1888
+ })
1889
+
1890
+ .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', '$transition', function($scope, $attrs, progressConfig, $transition) {
1891
+ var self = this,
1892
+ bars = [],
1893
+ max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max,
1894
+ animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
1895
+
1896
+ this.addBar = function(bar, element) {
1897
+ var oldValue = 0, index = bar.$parent.$index;
1898
+ if ( angular.isDefined(index) && bars[index] ) {
1899
+ oldValue = bars[index].value;
1900
+ }
1901
+ bars.push(bar);
1902
+
1903
+ this.update(element, bar.value, oldValue);
1904
+
1905
+ bar.$watch('value', function(value, oldValue) {
1906
+ if (value !== oldValue) {
1907
+ self.update(element, value, oldValue);
1908
+ }
1909
+ });
1910
+
1911
+ bar.$on('$destroy', function() {
1912
+ self.removeBar(bar);
1913
+ });
1914
+ };
1915
+
1916
+ // Update bar element width
1917
+ this.update = function(element, newValue, oldValue) {
1918
+ var percent = this.getPercentage(newValue);
1919
+
1920
+ if (animate) {
1921
+ element.css('width', this.getPercentage(oldValue) + '%');
1922
+ $transition(element, {width: percent + '%'});
1923
+ } else {
1924
+ element.css({'transition': 'none', 'width': percent + '%'});
1925
+ }
1926
+ };
1927
+
1928
+ this.removeBar = function(bar) {
1929
+ bars.splice(bars.indexOf(bar), 1);
1930
+ };
1931
+
1932
+ this.getPercentage = function(value) {
1933
+ return Math.round(100 * value / max);
1934
+ };
1935
+ }])
1936
+
1937
+ .directive('progress', function() {
1938
+ return {
1939
+ restrict: 'EA',
1940
+ replace: true,
1941
+ transclude: true,
1942
+ controller: 'ProgressController',
1943
+ require: 'progress',
1944
+ scope: {},
1945
+ template: '<div class="progress" ng-transclude></div>'
1946
+ //templateUrl: 'template/progressbar/progress.html' // Works in AngularJS 1.2
1947
+ };
1948
+ })
1949
+
1950
+ .directive('bar', function() {
1951
+ return {
1952
+ restrict: 'EA',
1953
+ replace: true,
1954
+ transclude: true,
1955
+ require: '^progress',
1956
+ scope: {
1957
+ value: '=',
1958
+ type: '@'
1959
+ },
1960
+ templateUrl: 'template/progressbar/bar.html',
1961
+ link: function(scope, element, attrs, progressCtrl) {
1962
+ progressCtrl.addBar(scope, element);
1963
+ }
1964
+ };
1965
+ })
1966
+
1967
+ .directive('progressbar', function() {
1968
+ return {
1969
+ restrict: 'EA',
1970
+ replace: true,
1971
+ transclude: true,
1972
+ controller: 'ProgressController',
1973
+ scope: {
1974
+ value: '=',
1975
+ type: '@'
1976
+ },
1977
+ templateUrl: 'template/progressbar/progressbar.html',
1978
+ link: function(scope, element, attrs, progressCtrl) {
1979
+ progressCtrl.addBar(scope, angular.element(element.children()[0]));
1980
+ }
1981
+ };
1982
+ });
1983
+
1984
+ angular.module('mm.foundation.rating', [])
1985
+
1986
+ .constant('ratingConfig', {
1987
+ max: 5,
1988
+ stateOn: null,
1989
+ stateOff: null
1990
+ })
1991
+
1992
+ .controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) {
1993
+
1994
+ this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max;
1995
+ this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
1996
+ this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
1997
+
1998
+ this.createRateObjects = function(states) {
1999
+ var defaultOptions = {
2000
+ stateOn: this.stateOn,
2001
+ stateOff: this.stateOff
2002
+ };
2003
+
2004
+ for (var i = 0, n = states.length; i < n; i++) {
2005
+ states[i] = angular.extend({ index: i }, defaultOptions, states[i]);
2006
+ }
2007
+ return states;
2008
+ };
2009
+
2010
+ // Get objects used in template
2011
+ $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange));
2012
+
2013
+ $scope.rate = function(value) {
2014
+ if ( $scope.value !== value && !$scope.readonly ) {
2015
+ $scope.value = value;
2016
+ }
2017
+ };
2018
+
2019
+ $scope.enter = function(value) {
2020
+ if ( ! $scope.readonly ) {
2021
+ $scope.val = value;
2022
+ }
2023
+ $scope.onHover({value: value});
2024
+ };
2025
+
2026
+ $scope.reset = function() {
2027
+ $scope.val = angular.copy($scope.value);
2028
+ $scope.onLeave();
2029
+ };
2030
+
2031
+ $scope.$watch('value', function(value) {
2032
+ $scope.val = value;
2033
+ });
2034
+
2035
+ $scope.readonly = false;
2036
+ if ($attrs.readonly) {
2037
+ $scope.$parent.$watch($parse($attrs.readonly), function(value) {
2038
+ $scope.readonly = !!value;
2039
+ });
2040
+ }
2041
+ }])
2042
+
2043
+ .directive('rating', function() {
2044
+ return {
2045
+ restrict: 'EA',
2046
+ scope: {
2047
+ value: '=',
2048
+ onHover: '&',
2049
+ onLeave: '&'
2050
+ },
2051
+ controller: 'RatingController',
2052
+ templateUrl: 'template/rating/rating.html',
2053
+ replace: true
2054
+ };
2055
+ });
2056
+
2057
+
2058
+ /**
2059
+ * @ngdoc overview
2060
+ * @name mm.foundation.tabs
2061
+ *
2062
+ * @description
2063
+ * AngularJS version of the tabs directive.
2064
+ */
2065
+
2066
+ angular.module('mm.foundation.tabs', [])
2067
+
2068
+ .controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
2069
+ var ctrl = this,
2070
+ tabs = ctrl.tabs = $scope.tabs = [];
2071
+
2072
+ ctrl.select = function(tab) {
2073
+ angular.forEach(tabs, function(tab) {
2074
+ tab.active = false;
2075
+ });
2076
+ tab.active = true;
2077
+ };
2078
+
2079
+ ctrl.addTab = function addTab(tab) {
2080
+ tabs.push(tab);
2081
+ if (tabs.length === 1 || tab.active) {
2082
+ ctrl.select(tab);
2083
+ }
2084
+ };
2085
+
2086
+ ctrl.removeTab = function removeTab(tab) {
2087
+ var index = tabs.indexOf(tab);
2088
+ //Select a new tab if the tab to be removed is selected
2089
+ if (tab.active && tabs.length > 1) {
2090
+ //If this is the last tab, select the previous tab. else, the next tab.
2091
+ var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
2092
+ ctrl.select(tabs[newActiveIndex]);
2093
+ }
2094
+ tabs.splice(index, 1);
2095
+ };
2096
+ }])
2097
+
2098
+ /**
2099
+ * @ngdoc directive
2100
+ * @name mm.foundation.tabs.directive:tabset
2101
+ * @restrict EA
2102
+ *
2103
+ * @description
2104
+ * Tabset is the outer container for the tabs directive
2105
+ *
2106
+ * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
2107
+ * @param {boolean=} justified Whether or not to use justified styling for the tabs.
2108
+ *
2109
+ * @example
2110
+ <example module="mm.foundation">
2111
+ <file name="index.html">
2112
+ <tabset>
2113
+ <tab heading="Tab 1"><b>First</b> Content!</tab>
2114
+ <tab heading="Tab 2"><i>Second</i> Content!</tab>
2115
+ </tabset>
2116
+ <hr />
2117
+ <tabset vertical="true">
2118
+ <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
2119
+ <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
2120
+ </tabset>
2121
+ <tabset justified="true">
2122
+ <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
2123
+ <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
2124
+ </tabset>
2125
+ </file>
2126
+ </example>
2127
+ */
2128
+ .directive('tabset', function() {
2129
+ return {
2130
+ restrict: 'EA',
2131
+ transclude: true,
2132
+ replace: true,
2133
+ scope: {},
2134
+ controller: 'TabsetController',
2135
+ templateUrl: 'template/tabs/tabset.html',
2136
+ link: function(scope, element, attrs) {
2137
+ scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
2138
+ scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
2139
+ scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs';
2140
+ }
2141
+ };
2142
+ })
2143
+
2144
+ /**
2145
+ * @ngdoc directive
2146
+ * @name mm.foundation.tabs.directive:tab
2147
+ * @restrict EA
2148
+ *
2149
+ * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link mm.foundation.tabs.directive:tabHeading tabHeading}.
2150
+ * @param {string=} select An expression to evaluate when the tab is selected.
2151
+ * @param {boolean=} active A binding, telling whether or not this tab is selected.
2152
+ * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
2153
+ *
2154
+ * @description
2155
+ * Creates a tab with a heading and content. Must be placed within a {@link mm.foundation.tabs.directive:tabset tabset}.
2156
+ *
2157
+ * @example
2158
+ <example module="mm.foundation">
2159
+ <file name="index.html">
2160
+ <div ng-controller="TabsDemoCtrl">
2161
+ <button class="button small" ng-click="items[0].active = true">
2162
+ Select item 1, using active binding
2163
+ </button>
2164
+ <button class="button small" ng-click="items[1].disabled = !items[1].disabled">
2165
+ Enable/disable item 2, using disabled binding
2166
+ </button>
2167
+ <br />
2168
+ <tabset>
2169
+ <tab heading="Tab 1">First Tab</tab>
2170
+ <tab select="alertMe()">
2171
+ <tab-heading><i class="fa fa-bell"></i> Alert me!</tab-heading>
2172
+ Second Tab, with alert callback and html heading!
2173
+ </tab>
2174
+ <tab ng-repeat="item in items"
2175
+ heading="{{item.title}}"
2176
+ disabled="item.disabled"
2177
+ active="item.active">
2178
+ {{item.content}}
2179
+ </tab>
2180
+ </tabset>
2181
+ </div>
2182
+ </file>
2183
+ <file name="script.js">
2184
+ function TabsDemoCtrl($scope) {
2185
+ $scope.items = [
2186
+ { title:"Dynamic Title 1", content:"Dynamic Item 0" },
2187
+ { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
2188
+ ];
2189
+
2190
+ $scope.alertMe = function() {
2191
+ setTimeout(function() {
2192
+ alert("You've selected the alert tab!");
2193
+ });
2194
+ };
2195
+ };
2196
+ </file>
2197
+ </example>
2198
+ */
2199
+
2200
+ /**
2201
+ * @ngdoc directive
2202
+ * @name mm.foundation.tabs.directive:tabHeading
2203
+ * @restrict EA
2204
+ *
2205
+ * @description
2206
+ * Creates an HTML heading for a {@link mm.foundation.tabs.directive:tab tab}. Must be placed as a child of a tab element.
2207
+ *
2208
+ * @example
2209
+ <example module="mm.foundation">
2210
+ <file name="index.html">
2211
+ <tabset>
2212
+ <tab>
2213
+ <tab-heading><b>HTML</b> in my titles?!</tab-heading>
2214
+ And some content, too!
2215
+ </tab>
2216
+ <tab>
2217
+ <tab-heading><i class="fa fa-heart"></i> Icon heading?!?</tab-heading>
2218
+ That's right.
2219
+ </tab>
2220
+ </tabset>
2221
+ </file>
2222
+ </example>
2223
+ */
2224
+ .directive('tab', ['$parse', function($parse) {
2225
+ return {
2226
+ require: '^tabset',
2227
+ restrict: 'EA',
2228
+ replace: true,
2229
+ templateUrl: 'template/tabs/tab.html',
2230
+ transclude: true,
2231
+ scope: {
2232
+ heading: '@',
2233
+ onSelect: '&select', //This callback is called in contentHeadingTransclude
2234
+ //once it inserts the tab's content into the dom
2235
+ onDeselect: '&deselect'
2236
+ },
2237
+ controller: function() {
2238
+ //Empty controller so other directives can require being 'under' a tab
2239
+ },
2240
+ compile: function(elm, attrs, transclude) {
2241
+ return function postLink(scope, elm, attrs, tabsetCtrl) {
2242
+ var getActive, setActive;
2243
+ if (attrs.active) {
2244
+ getActive = $parse(attrs.active);
2245
+ setActive = getActive.assign;
2246
+ scope.$parent.$watch(getActive, function updateActive(value, oldVal) {
2247
+ // Avoid re-initializing scope.active as it is already initialized
2248
+ // below. (watcher is called async during init with value ===
2249
+ // oldVal)
2250
+ if (value !== oldVal) {
2251
+ scope.active = !!value;
2252
+ }
2253
+ });
2254
+ scope.active = getActive(scope.$parent);
2255
+ } else {
2256
+ setActive = getActive = angular.noop;
2257
+ }
2258
+
2259
+ scope.$watch('active', function(active) {
2260
+ // Note this watcher also initializes and assigns scope.active to the
2261
+ // attrs.active expression.
2262
+ setActive(scope.$parent, active);
2263
+ if (active) {
2264
+ tabsetCtrl.select(scope);
2265
+ scope.onSelect();
2266
+ } else {
2267
+ scope.onDeselect();
2268
+ }
2269
+ });
2270
+
2271
+ scope.disabled = false;
2272
+ if ( attrs.disabled ) {
2273
+ scope.$parent.$watch($parse(attrs.disabled), function(value) {
2274
+ scope.disabled = !! value;
2275
+ });
2276
+ }
2277
+
2278
+ scope.select = function() {
2279
+ if ( ! scope.disabled ) {
2280
+ scope.active = true;
2281
+ }
2282
+ };
2283
+
2284
+ tabsetCtrl.addTab(scope);
2285
+ scope.$on('$destroy', function() {
2286
+ tabsetCtrl.removeTab(scope);
2287
+ });
2288
+
2289
+
2290
+ //We need to transclude later, once the content container is ready.
2291
+ //when this link happens, we're inside a tab heading.
2292
+ scope.$transcludeFn = transclude;
2293
+ };
2294
+ }
2295
+ };
2296
+ }])
2297
+
2298
+ .directive('tabHeadingTransclude', [function() {
2299
+ return {
2300
+ restrict: 'A',
2301
+ require: '^tab',
2302
+ link: function(scope, elm, attrs, tabCtrl) {
2303
+ scope.$watch('headingElement', function updateHeadingElement(heading) {
2304
+ if (heading) {
2305
+ elm.html('');
2306
+ elm.append(heading);
2307
+ }
2308
+ });
2309
+ }
2310
+ };
2311
+ }])
2312
+
2313
+ .directive('tabContentTransclude', function() {
2314
+ return {
2315
+ restrict: 'A',
2316
+ require: '^tabset',
2317
+ link: function(scope, elm, attrs) {
2318
+ var tab = scope.$eval(attrs.tabContentTransclude);
2319
+
2320
+ //Now our tab is ready to be transcluded: both the tab heading area
2321
+ //and the tab content area are loaded. Transclude 'em both.
2322
+ tab.$transcludeFn(tab.$parent, function(contents) {
2323
+ angular.forEach(contents, function(node) {
2324
+ if (isTabHeading(node)) {
2325
+ //Let tabHeadingTransclude know.
2326
+ tab.headingElement = node;
2327
+ } else {
2328
+ elm.append(node);
2329
+ }
2330
+ });
2331
+ });
2332
+ }
2333
+ };
2334
+ function isTabHeading(node) {
2335
+ return node.tagName && (
2336
+ node.hasAttribute('tab-heading') ||
2337
+ node.hasAttribute('data-tab-heading') ||
2338
+ node.tagName.toLowerCase() === 'tab-heading' ||
2339
+ node.tagName.toLowerCase() === 'data-tab-heading'
2340
+ );
2341
+ }
2342
+ })
2343
+
2344
+ ;
2345
+
2346
+
2347
+ angular.module("mm.foundation.topbar", [])
2348
+ .factory('mediaQueries', ['$document', '$window', function($document, $window){
2349
+ var head = angular.element($document[0].querySelector('head'));
2350
+ head.append('<meta class="foundation-mq-topbar" />');
2351
+ head.append('<meta class="foundation-mq-small" />');
2352
+ head.append('<meta class="foundation-mq-medium" />');
2353
+ head.append('<meta class="foundation-mq-large" />');
2354
+
2355
+ // MatchMedia for IE <= 9
2356
+ var matchMedia = $window.matchMedia || (function(doc, undefined){
2357
+ var bool,
2358
+ docElem = doc.documentElement,
2359
+ refNode = docElem.firstElementChild || docElem.firstChild,
2360
+ // fakeBody required for <FF4 when executed in <head>
2361
+ fakeBody = doc.createElement("body"),
2362
+ div = doc.createElement("div");
2363
+
2364
+ div.id = "mq-test-1";
2365
+ div.style.cssText = "position:absolute;top:-100em";
2366
+ fakeBody.style.background = "none";
2367
+ fakeBody.appendChild(div);
2368
+
2369
+ return function (q) {
2370
+ div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
2371
+ docElem.insertBefore(fakeBody, refNode);
2372
+ bool = div.offsetWidth === 42;
2373
+ docElem.removeChild(fakeBody);
2374
+ return {
2375
+ matches: bool,
2376
+ media: q
2377
+ };
2378
+ };
2379
+
2380
+ }($document[0]));
2381
+
2382
+ var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g;
2383
+ var queries = {
2384
+ topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''),
2385
+ small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''),
2386
+ medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''),
2387
+ large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '')
2388
+ };
2389
+
2390
+ return {
2391
+ topbarBreakpoint: function () {
2392
+ return !matchMedia(queries.topbar).matches;
2393
+ },
2394
+ small: function () {
2395
+ return matchMedia(queries.small).matches;
2396
+ },
2397
+ medium: function () {
2398
+ return matchMedia(queries.medium).matches;
2399
+ },
2400
+ large: function () {
2401
+ return matchMedia(queries.large).matches;
2402
+ }
2403
+ };
2404
+
2405
+ }])
2406
+ .factory('closest', [function(){
2407
+ return function(el, selector) {
2408
+ var matchesSelector = function (node, selector) {
2409
+ var nodes = (node.parentNode || node.document).querySelectorAll(selector);
2410
+ var i = -1;
2411
+ while (nodes[++i] && nodes[i] != node){}
2412
+ return !!nodes[i];
2413
+ };
2414
+
2415
+ var element = el[0];
2416
+ while (element) {
2417
+ if (matchesSelector(element, selector)) {
2418
+ return angular.element(element);
2419
+ } else {
2420
+ element = element.parentElement;
2421
+ }
2422
+ }
2423
+ return false;
2424
+ };
2425
+ }])
2426
+ .directive('topBar', ['$timeout','$compile', '$window', '$document', 'mediaQueries',
2427
+ function ($timeout, $compile, $window, $document, mediaQueries) {
2428
+ return {
2429
+ scope: {
2430
+ stickyClass : '@',
2431
+ backText: '@',
2432
+ stickyOn : '=',
2433
+ customBackText: '=',
2434
+ isHover: '=',
2435
+ mobileShowParentLink: '=',
2436
+ scrolltop : '=',
2437
+ },
2438
+ restrict: 'EA',
2439
+ replace: true,
2440
+ templateUrl: 'template/topbar/top-bar.html',
2441
+ transclude: true,
2442
+ link: function ($scope, element, attrs) {
2443
+ var topbar = $scope.topbar = element;
2444
+ var topbarContainer = topbar.parent();
2445
+ var body = angular.element($document[0].querySelector('body'));
2446
+
2447
+ var isSticky = $scope.isSticky = function () {
2448
+ var sticky = topbarContainer.hasClass($scope.settings.stickyClass);
2449
+ if (sticky && $scope.settings.stickyOn === 'all') {
2450
+ return true;
2451
+ } else if (sticky && mediaQueries.small() && $scope.settings.stickyOn === 'small') {
2452
+ return true;
2453
+ } else if (sticky && mediaQueries.medium() && $scope.settings.stickyOn === 'medium') {
2454
+ return true;
2455
+ } else if (sticky && mediaQueries.large() && $scope.settings.stickyOn === 'large') {
2456
+ return true;
2457
+ }
2458
+ return false;
2459
+ };
2460
+
2461
+ var updateStickyPositioning = function(){
2462
+ if (!$scope.stickyTopbar || !$scope.isSticky()) {
2463
+ return;
2464
+ }
2465
+
2466
+ var $class = angular.element($document[0].querySelector('.' + $scope.settings.stickyClass));
2467
+ var distance = stickyoffset;
2468
+
2469
+ if ($window.scrollY > distance && !$class.hasClass('fixed')) {
2470
+ $class.addClass('fixed');
2471
+ body.css('padding-top', $scope.originalHeight + 'px');
2472
+ } else if ($window.scrollY <= distance && $class.hasClass('fixed')) {
2473
+ $class.removeClass('fixed');
2474
+ body.css('padding-top', '');
2475
+ }
2476
+ };
2477
+
2478
+ $scope.toggle = function(on) {
2479
+ if(!mediaQueries.topbarBreakpoint()){
2480
+ return false;
2481
+ }
2482
+
2483
+ var expand = (on === undefined) ? !topbar.hasClass('expanded') : on;
2484
+
2485
+ if (expand){
2486
+ topbar.addClass('expanded');
2487
+ }
2488
+ else {
2489
+ topbar.removeClass('expanded');
2490
+ }
2491
+
2492
+ if ($scope.settings.scrolltop) {
2493
+ if (!expand && topbar.hasClass('fixed')) {
2494
+ topbar.parent().addClass('fixed');
2495
+ topbar.removeClass('fixed');
2496
+ body.css('padding-top', $scope.originalHeight + 'px');
2497
+ } else if (expand && topbar.parent().hasClass('fixed')) {
2498
+ topbar.parent().removeClass('fixed');
2499
+ topbar.addClass('fixed');
2500
+ body.css('padding-top', '');
2501
+ $window.scrollTo(0,0);
2502
+ }
2503
+ } else {
2504
+ if(isSticky()) {
2505
+ topbar.parent().addClass('fixed');
2506
+ }
2507
+
2508
+ if(topbar.parent().hasClass('fixed')) {
2509
+ if (!expand) {
2510
+ topbar.removeClass('fixed');
2511
+ topbar.parent().removeClass('expanded');
2512
+ updateStickyPositioning();
2513
+ } else {
2514
+ topbar.addClass('fixed');
2515
+ topbar.parent().addClass('expanded');
2516
+ body.css('padding-top', $scope.originalHeight + 'px');
2517
+ }
2518
+ }
2519
+ }
2520
+ };
2521
+
2522
+ if(topbarContainer.hasClass('fixed') || isSticky() ) {
2523
+ $scope.stickyTopbar = true;
2524
+ $scope.height = topbarContainer[0].offsetHeight;
2525
+ var stickyoffset = topbarContainer[0].getBoundingClientRect().top;
2526
+ } else {
2527
+ $scope.height = topbar[0].offsetHeight;
2528
+ }
2529
+
2530
+ $scope.originalHeight = $scope.height;
2531
+
2532
+ $scope.$watch('height', function(h){
2533
+ if(h){
2534
+ topbar.css('height', h + 'px');
2535
+ } else {
2536
+ topbar.css('height', '');
2537
+ }
2538
+ });
2539
+
2540
+ var lastBreakpoint = mediaQueries.topbarBreakpoint();
2541
+
2542
+ angular.element($window).bind('resize', function(){
2543
+ var currentBreakpoint = mediaQueries.topbarBreakpoint();
2544
+ if(lastBreakpoint === currentBreakpoint){
2545
+ return;
2546
+ }
2547
+ lastBreakpoint = mediaQueries.topbarBreakpoint();
2548
+
2549
+ topbar.removeClass('expanded');
2550
+ topbar.parent().removeClass('expanded');
2551
+ $scope.height = '';
2552
+
2553
+ var sections = angular.element(topbar[0].querySelectorAll('section'));
2554
+ angular.forEach(sections, function(section){
2555
+ angular.element(section.querySelectorAll('li.moved')).removeClass('moved');
2556
+ });
2557
+
2558
+ $scope.$apply();
2559
+ });
2560
+
2561
+ // update sticky positioning
2562
+ angular.element($window).bind("scroll", function() {
2563
+ updateStickyPositioning();
2564
+ $scope.$apply();
2565
+ });
2566
+
2567
+ $scope.$on('$destroy', function(){
2568
+ angular.element($window).unbind("scroll");
2569
+ angular.element($window).unbind("resize");
2570
+ });
2571
+
2572
+ if (topbarContainer.hasClass('fixed')) {
2573
+ body.css('padding-top', $scope.originalHeight + 'px');
2574
+ }
2575
+
2576
+ },
2577
+ controller: ['$window', '$scope', 'closest', function($window, $scope, closest) {
2578
+ $scope.settings = {};
2579
+ $scope.settings.stickyClass = $scope.stickyClass || 'sticky';
2580
+ $scope.settings.backText = $scope.backText || 'Back';
2581
+ $scope.settings.stickyOn = $scope.stickyOn || 'all';
2582
+
2583
+ $scope.settings.customBackText = $scope.customBackText === undefined ? true : $scope.customBackText;
2584
+ $scope.settings.isHover = $scope.isHover === undefined ? true : $scope.isHover;
2585
+ $scope.settings.mobileShowParentLink = $scope.mobileShowParentLink === undefined ? true : $scope.mobileShowParentLink;
2586
+ $scope.settings.scrolltop = $scope.scrolltop === undefined ? true : $scope.scrolltop; // jump to top when sticky nav menu toggle is clicked
2587
+
2588
+ this.settings = $scope.settings;
2589
+
2590
+ $scope.index = 0;
2591
+
2592
+ var outerHeight = function(el){
2593
+ var height = el.offsetHeight;
2594
+ var style = el.currentStyle || getComputedStyle(el);
2595
+
2596
+ height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);
2597
+ return height;
2598
+ };
2599
+
2600
+
2601
+ var sections = [];
2602
+
2603
+ this.addSection = function(section){
2604
+ sections.push(section);
2605
+ };
2606
+
2607
+ this.removeSection = function(section){
2608
+ var index = sections.indexOf(section);
2609
+ if (index > -1) {
2610
+ sections.splice(index, 1);
2611
+ }
2612
+ };
2613
+
2614
+ var dir = /rtl/i.test($document.find('html').attr('dir')) ? 'right' : 'left';
2615
+
2616
+ $scope.$watch('index', function(index){
2617
+ for(var i = 0; i < sections.length; i++){
2618
+ sections[i].move(dir, index);
2619
+ }
2620
+ });
2621
+
2622
+ this.toggle = function(on){
2623
+ $scope.toggle(on);
2624
+ for(var i = 0; i < sections.length; i++){
2625
+ sections[i].reset();
2626
+ }
2627
+ $scope.index = 0;
2628
+ $scope.height = '';
2629
+ $scope.$apply();
2630
+ };
2631
+
2632
+ this.back = function(event) {
2633
+ if($scope.index < 1 || !mediaQueries.topbarBreakpoint()){
2634
+ return;
2635
+ }
2636
+
2637
+ var $link = angular.element(event.currentTarget);
2638
+ var $movedLi = closest($link, 'li.moved');
2639
+ var $previousLevelUl = $movedLi.parent();
2640
+ $scope.index = $scope.index -1;
2641
+
2642
+ if($scope.index === 0){
2643
+ $scope.height = '';
2644
+ } else {
2645
+ $scope.height = $scope.originalHeight + outerHeight($previousLevelUl[0]);
2646
+ }
2647
+
2648
+ $timeout(function () {
2649
+ $movedLi.removeClass('moved');
2650
+ }, 300);
2651
+ };
2652
+
2653
+ this.forward = function(event) {
2654
+ if(!mediaQueries.topbarBreakpoint()){
2655
+ return false;
2656
+ }
2657
+
2658
+ var $link = angular.element(event.currentTarget);
2659
+ var $selectedLi = closest($link, 'li');
2660
+ $selectedLi.addClass('moved');
2661
+ $scope.height = $scope.originalHeight + outerHeight($link.parent()[0].querySelector('ul'));
2662
+ $scope.index = $scope.index + 1;
2663
+ $scope.$apply();
2664
+ };
2665
+
2666
+ }]
2667
+ };
2668
+ }])
2669
+ .directive('toggleTopBar', ['closest', function (closest) {
2670
+ return {
2671
+ scope: {},
2672
+ require: '^topBar',
2673
+ restrict: 'A',
2674
+ replace: true,
2675
+ templateUrl: 'template/topbar/toggle-top-bar.html',
2676
+ transclude: true,
2677
+ link: function ($scope, element, attrs, topBar) {
2678
+ element.bind('click', function(event) {
2679
+ var li = closest(angular.element(event.currentTarget), 'li');
2680
+ if(!li.hasClass('back') && !li.hasClass('has-dropdown')) {
2681
+ topBar.toggle();
2682
+ }
2683
+ });
2684
+
2685
+ $scope.$on('$destroy', function(){
2686
+ element.unbind('click');
2687
+ });
2688
+ }
2689
+ };
2690
+ }])
2691
+ .directive('topBarSection', ['$compile', 'closest', function ($compile, closest) {
2692
+ return {
2693
+ scope: {},
2694
+ require: '^topBar',
2695
+ restrict: 'EA',
2696
+ replace: true,
2697
+ templateUrl: 'template/topbar/top-bar-section.html',
2698
+ transclude: true,
2699
+ link: function ($scope, element, attrs, topBar) {
2700
+ var section = element;
2701
+
2702
+ $scope.reset = function(){
2703
+ angular.element(section[0].querySelectorAll('li.moved')).removeClass('moved');
2704
+ };
2705
+
2706
+ $scope.move = function(dir, index){
2707
+ if(dir === 'left'){
2708
+ section.css({"left": index * -100 + '%'});
2709
+ }
2710
+ else {
2711
+ section.css({"right": index * -100 + '%'});
2712
+ }
2713
+ };
2714
+
2715
+ topBar.addSection($scope);
2716
+
2717
+ $scope.$on("$destroy", function(){
2718
+ topBar.removeSection($scope);
2719
+ });
2720
+
2721
+ // Top level links close menu on click
2722
+ var links = section[0].querySelectorAll('li>a');
2723
+ angular.forEach(links, function(link){
2724
+ var $link = angular.element(link);
2725
+ var li = closest($link, 'li');
2726
+ if(li.hasClass('has-dropdown') || li.hasClass('back') || li.hasClass('title')){
2727
+ return;
2728
+ }
2729
+
2730
+ $link.bind('click', function(){
2731
+ topBar.toggle(false);
2732
+ });
2733
+
2734
+ $scope.$on('$destroy', function(){
2735
+ $link.bind('click');
2736
+ });
2737
+ });
2738
+
2739
+ }
2740
+ };
2741
+ }])
2742
+ .directive('hasDropdown', ['mediaQueries', function (mediaQueries) {
2743
+ return {
2744
+ scope: {},
2745
+ require: '^topBar',
2746
+ restrict: 'A',
2747
+ templateUrl: 'template/topbar/has-dropdown.html',
2748
+ replace: true,
2749
+ transclude: true,
2750
+ link: function ($scope, element, attrs, topBar) {
2751
+ $scope.triggerLink = element.children('a')[0];
2752
+
2753
+ var $link = angular.element($scope.triggerLink);
2754
+
2755
+ $link.bind('click', function(event){
2756
+ topBar.forward(event);
2757
+ });
2758
+ $scope.$on('$destroy', function(){
2759
+ $link.unbind('click');
2760
+ });
2761
+
2762
+ element.bind('mouseenter', function() {
2763
+ if(topBar.settings.isHover && !mediaQueries.topbarBreakpoint()){
2764
+ element.addClass('not-click');
2765
+ }
2766
+ });
2767
+ element.bind('click', function(event) {
2768
+ if(!topBar.settings.isHover && !mediaQueries.topbarBreakpoint()){
2769
+ element.toggleClass('not-click');
2770
+ }
2771
+ });
2772
+
2773
+ element.bind('mouseleave', function() {
2774
+ element.removeClass('not-click');
2775
+ });
2776
+
2777
+ $scope.$on('$destroy', function(){
2778
+ element.unbind('click');
2779
+ element.unbind('mouseenter');
2780
+ element.unbind('mouseleave');
2781
+ });
2782
+ },
2783
+ controller: ['$window', '$scope', function($window, $scope) {
2784
+ this.triggerLink = $scope.triggerLink;
2785
+ }]
2786
+ };
2787
+ }])
2788
+ .directive('topBarDropdown', ['$compile', function ($compile) {
2789
+ return {
2790
+ scope: {},
2791
+ require: ['^topBar', '^hasDropdown'],
2792
+ restrict: 'A',
2793
+ replace: true,
2794
+ templateUrl: 'template/topbar/top-bar-dropdown.html',
2795
+ transclude: true,
2796
+ link: function ($scope, element, attrs, ctrls) {
2797
+
2798
+ var topBar = ctrls[0];
2799
+ var hasDropdown = ctrls[1];
2800
+
2801
+ var $link = angular.element(hasDropdown.triggerLink);
2802
+ var url = $link.attr('href');
2803
+
2804
+ $scope.linkText = $link.text();
2805
+
2806
+ $scope.back = function(event){
2807
+ topBar.back(event);
2808
+ };
2809
+
2810
+ // Add back link
2811
+ if (topBar.settings.customBackText) {
2812
+ $scope.backText = topBar.settings.backText;
2813
+ } else {
2814
+ $scope.backText = '&laquo; ' + $link.html();
2815
+ }
2816
+
2817
+ var $titleLi;
2818
+ if (topBar.settings.mobileShowParentLink && url && url.length > 1) {
2819
+ $titleLi = angular.element('<li class="title back js-generated">' +
2820
+ '<h5><a href="#" ng-click="back($event);">{{backText}}</a></h5></li>' +
2821
+ '<li><a class="parent-link js-generated" href="' +
2822
+ url + '">{{linkText}}</a></li>');
2823
+ } else {
2824
+ $titleLi = angular.element('<li class="title back js-generated">' +
2825
+ '<h5><a href="" ng-click="back($event);">{{backText}}</a></h5></li>');
2826
+ }
2827
+
2828
+ $compile($titleLi)($scope);
2829
+ element.prepend($titleLi);
2830
+ }
2831
+ };
2832
+ }]);
2833
+
2834
+ angular.module( 'mm.foundation.tour', [ 'mm.foundation.position', 'mm.foundation.tooltip' ] )
2835
+
2836
+ .service( '$tour', [ '$window', function ( $window ) {
2837
+ var currentIndex = getCurrentStep();
2838
+ var ended = false;
2839
+ var steps = {};
2840
+
2841
+ function getCurrentStep() {
2842
+ return parseInt( $window.localStorage.getItem( 'mm.tour.step' ), 10 );
2843
+ }
2844
+
2845
+ function setCurrentStep(step) {
2846
+ currentIndex = step;
2847
+ $window.localStorage.setItem( 'mm.tour.step', step );
2848
+ }
2849
+
2850
+ this.add = function ( index, attrs ) {
2851
+ steps[ index ] = attrs;
2852
+ };
2853
+
2854
+ this.has = function ( index ) {
2855
+ return !!steps[ index ];
2856
+ };
2857
+
2858
+ this.isActive = function () {
2859
+ return currentIndex > 0;
2860
+ };
2861
+
2862
+ this.current = function ( index ) {
2863
+ if ( index ) {
2864
+ setCurrentStep( currentIndex );
2865
+ } else {
2866
+ return currentIndex;
2867
+ }
2868
+ };
2869
+
2870
+ this.start = function () {
2871
+ setCurrentStep( 1 );
2872
+ };
2873
+
2874
+ this.next = function () {
2875
+ setCurrentStep( currentIndex + 1 );
2876
+ };
2877
+
2878
+ this.end = function () {
2879
+ setCurrentStep( 0 );
2880
+ };
2881
+ }])
2882
+
2883
+ .directive( 'stepTextPopup', ['$tour', function ( $tour ) {
2884
+ return {
2885
+ restrict: 'EA',
2886
+ replace: true,
2887
+ scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
2888
+ templateUrl: 'template/tour/tour.html',
2889
+ link: function (scope, element) {
2890
+ scope.isLastStep = function () {
2891
+ return !$tour.has( $tour.current() + 1 );
2892
+ };
2893
+
2894
+ scope.endTour = function () {
2895
+ element.remove();
2896
+ $tour.end();
2897
+ };
2898
+
2899
+ scope.nextStep = function () {
2900
+ element.remove();
2901
+ $tour.next();
2902
+ };
2903
+ }
2904
+ };
2905
+ }])
2906
+
2907
+ .directive( 'stepText', [ '$position', '$tooltip', '$tour', '$window', function ( $position, $tooltip, $tour, $window ) {
2908
+ function isElementInViewport( element ) {
2909
+ var rect = element[0].getBoundingClientRect();
2910
+
2911
+ return (
2912
+ rect.top >= 0 &&
2913
+ rect.left >= 0 &&
2914
+ rect.bottom <= ($window.innerHeight - 80) &&
2915
+ rect.right <= $window.innerWidth
2916
+ );
2917
+ }
2918
+
2919
+ function show( scope, element, attrs ) {
2920
+ var index = parseInt( attrs.stepIndex, 10);
2921
+
2922
+ if ( $tour.isActive() && index ) {
2923
+ $tour.add( index, attrs );
2924
+
2925
+ if ( index === $tour.current() ) {
2926
+ if ( !isElementInViewport( element ) ) {
2927
+ var offset = $position.offset( element );
2928
+ $window.scrollTo( 0, offset.top - $window.innerHeight / 2 );
2929
+ }
2930
+
2931
+ return true;
2932
+ }
2933
+ }
2934
+
2935
+ return false;
2936
+ }
2937
+
2938
+ return $tooltip( 'stepText', 'step', show );
2939
+ }]);
2940
+
2941
+ angular.module('mm.foundation.typeahead', ['mm.foundation.position', 'mm.foundation.bindHtml'])
2942
+
2943
+ /**
2944
+ * A helper service that can parse typeahead's syntax (string provided by users)
2945
+ * Extracted to a separate service for ease of unit testing
2946
+ */
2947
+ .factory('typeaheadParser', ['$parse', function ($parse) {
2948
+
2949
+ // 00000111000000000000022200000000000000003333333333333330000000000044000
2950
+ var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
2951
+
2952
+ return {
2953
+ parse:function (input) {
2954
+
2955
+ var match = input.match(TYPEAHEAD_REGEXP);
2956
+ if (!match) {
2957
+ throw new Error(
2958
+ "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
2959
+ " but got '" + input + "'.");
2960
+ }
2961
+
2962
+ return {
2963
+ itemName:match[3],
2964
+ source:$parse(match[4]),
2965
+ viewMapper:$parse(match[2] || match[1]),
2966
+ modelMapper:$parse(match[1])
2967
+ };
2968
+ }
2969
+ };
2970
+ }])
2971
+
2972
+ .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
2973
+ function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {
2974
+
2975
+ var HOT_KEYS = [9, 13, 27, 38, 40];
2976
+
2977
+ return {
2978
+ require:'ngModel',
2979
+ link:function (originalScope, element, attrs, modelCtrl) {
2980
+
2981
+ //SUPPORTED ATTRIBUTES (OPTIONS)
2982
+
2983
+ //minimal no of characters that needs to be entered before typeahead kicks-in
2984
+ var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
2985
+
2986
+ //minimal wait time after last character typed before typehead kicks-in
2987
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
2988
+
2989
+ //should it restrict model values to the ones selected from the popup only?
2990
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
2991
+
2992
+ //binding to a variable that indicates if matches are being retrieved asynchronously
2993
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
2994
+
2995
+ //a callback executed when a match is selected
2996
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
2997
+
2998
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
2999
+
3000
+ var appendToBody = attrs.typeaheadAppendToBody ? $parse(attrs.typeaheadAppendToBody) : false;
3001
+
3002
+ //INTERNAL VARIABLES
3003
+
3004
+ //model setter executed upon match selection
3005
+ var $setModelValue = $parse(attrs.ngModel).assign;
3006
+
3007
+ //expressions used by typeahead
3008
+ var parserResult = typeaheadParser.parse(attrs.typeahead);
3009
+
3010
+ var hasFocus;
3011
+
3012
+ //pop-up element used to display matches
3013
+ var popUpEl = angular.element('<div typeahead-popup></div>');
3014
+ popUpEl.attr({
3015
+ matches: 'matches',
3016
+ active: 'activeIdx',
3017
+ select: 'select(activeIdx)',
3018
+ query: 'query',
3019
+ position: 'position'
3020
+ });
3021
+ //custom item template
3022
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
3023
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
3024
+ }
3025
+
3026
+ //create a child scope for the typeahead directive so we are not polluting original scope
3027
+ //with typeahead-specific data (matches, query etc.)
3028
+ var scope = originalScope.$new();
3029
+ originalScope.$on('$destroy', function(){
3030
+ scope.$destroy();
3031
+ });
3032
+
3033
+ var resetMatches = function() {
3034
+ scope.matches = [];
3035
+ scope.activeIdx = -1;
3036
+ };
3037
+
3038
+ var getMatchesAsync = function(inputValue) {
3039
+
3040
+ var locals = {$viewValue: inputValue};
3041
+ isLoadingSetter(originalScope, true);
3042
+ $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
3043
+
3044
+ //it might happen that several async queries were in progress if a user were typing fast
3045
+ //but we are interested only in responses that correspond to the current view value
3046
+ if (inputValue === modelCtrl.$viewValue && hasFocus) {
3047
+ if (matches.length > 0) {
3048
+
3049
+ scope.activeIdx = 0;
3050
+ scope.matches.length = 0;
3051
+
3052
+ //transform labels
3053
+ for(var i=0; i<matches.length; i++) {
3054
+ locals[parserResult.itemName] = matches[i];
3055
+ scope.matches.push({
3056
+ label: parserResult.viewMapper(scope, locals),
3057
+ model: matches[i]
3058
+ });
3059
+ }
3060
+
3061
+ scope.query = inputValue;
3062
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
3063
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
3064
+ //due to other elements being rendered
3065
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
3066
+ scope.position.top = scope.position.top + element.prop('offsetHeight');
3067
+
3068
+ } else {
3069
+ resetMatches();
3070
+ }
3071
+ isLoadingSetter(originalScope, false);
3072
+ }
3073
+ }, function(){
3074
+ resetMatches();
3075
+ isLoadingSetter(originalScope, false);
3076
+ });
3077
+ };
3078
+
3079
+ resetMatches();
3080
+
3081
+ //we need to propagate user's query so we can higlight matches
3082
+ scope.query = undefined;
3083
+
3084
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
3085
+ var timeoutPromise;
3086
+
3087
+ //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
3088
+ //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
3089
+ modelCtrl.$parsers.unshift(function (inputValue) {
3090
+
3091
+ if (inputValue && inputValue.length >= minSearch) {
3092
+ if (waitTime > 0) {
3093
+ if (timeoutPromise) {
3094
+ $timeout.cancel(timeoutPromise);//cancel previous timeout
3095
+ }
3096
+ timeoutPromise = $timeout(function () {
3097
+ getMatchesAsync(inputValue);
3098
+ }, waitTime);
3099
+ } else {
3100
+ getMatchesAsync(inputValue);
3101
+ }
3102
+ } else {
3103
+ isLoadingSetter(originalScope, false);
3104
+ resetMatches();
3105
+ }
3106
+
3107
+ if (isEditable) {
3108
+ return inputValue;
3109
+ } else {
3110
+ if (!inputValue) {
3111
+ // Reset in case user had typed something previously.
3112
+ modelCtrl.$setValidity('editable', true);
3113
+ return inputValue;
3114
+ } else {
3115
+ modelCtrl.$setValidity('editable', false);
3116
+ return undefined;
3117
+ }
3118
+ }
3119
+ });
3120
+
3121
+ modelCtrl.$formatters.push(function (modelValue) {
3122
+
3123
+ var candidateViewValue, emptyViewValue;
3124
+ var locals = {};
3125
+
3126
+ if (inputFormatter) {
3127
+
3128
+ locals['$model'] = modelValue;
3129
+ return inputFormatter(originalScope, locals);
3130
+
3131
+ } else {
3132
+
3133
+ //it might happen that we don't have enough info to properly render input value
3134
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
3135
+ locals[parserResult.itemName] = modelValue;
3136
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
3137
+ locals[parserResult.itemName] = undefined;
3138
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
3139
+
3140
+ return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
3141
+ }
3142
+ });
3143
+
3144
+ scope.select = function (activeIdx) {
3145
+ //called from within the $digest() cycle
3146
+ var locals = {};
3147
+ var model, item;
3148
+
3149
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
3150
+ model = parserResult.modelMapper(originalScope, locals);
3151
+ $setModelValue(originalScope, model);
3152
+ modelCtrl.$setValidity('editable', true);
3153
+
3154
+ onSelectCallback(originalScope, {
3155
+ $item: item,
3156
+ $model: model,
3157
+ $label: parserResult.viewMapper(originalScope, locals)
3158
+ });
3159
+
3160
+ resetMatches();
3161
+
3162
+ //return focus to the input element if a mach was selected via a mouse click event
3163
+ element[0].focus();
3164
+ };
3165
+
3166
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
3167
+ element.bind('keydown', function (evt) {
3168
+
3169
+ //typeahead is open and an "interesting" key was pressed
3170
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
3171
+ return;
3172
+ }
3173
+
3174
+ evt.preventDefault();
3175
+
3176
+ if (evt.which === 40) {
3177
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
3178
+ scope.$digest();
3179
+
3180
+ } else if (evt.which === 38) {
3181
+ scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
3182
+ scope.$digest();
3183
+
3184
+ } else if (evt.which === 13 || evt.which === 9) {
3185
+ scope.$apply(function () {
3186
+ scope.select(scope.activeIdx);
3187
+ });
3188
+
3189
+ } else if (evt.which === 27) {
3190
+ evt.stopPropagation();
3191
+
3192
+ resetMatches();
3193
+ scope.$digest();
3194
+ }
3195
+ });
3196
+
3197
+ element.bind('blur', function (evt) {
3198
+ hasFocus = false;
3199
+ });
3200
+
3201
+ element.bind('focus', function (evt) {
3202
+ hasFocus = true;
3203
+ });
3204
+
3205
+ // Keep reference to click handler to unbind it.
3206
+ var dismissClickHandler = function (evt) {
3207
+ if (element[0] !== evt.target) {
3208
+ resetMatches();
3209
+ scope.$digest();
3210
+ }
3211
+ };
3212
+
3213
+ $document.bind('click', dismissClickHandler);
3214
+
3215
+ originalScope.$on('$destroy', function(){
3216
+ $document.unbind('click', dismissClickHandler);
3217
+ });
3218
+
3219
+ var $popup = $compile(popUpEl)(scope);
3220
+ if ( appendToBody ) {
3221
+ $document.find('body').append($popup);
3222
+ } else {
3223
+ element.after($popup);
3224
+ }
3225
+ }
3226
+ };
3227
+
3228
+ }])
3229
+
3230
+ .directive('typeaheadPopup', function () {
3231
+ return {
3232
+ restrict:'EA',
3233
+ scope:{
3234
+ matches:'=',
3235
+ query:'=',
3236
+ active:'=',
3237
+ position:'=',
3238
+ select:'&'
3239
+ },
3240
+ replace:true,
3241
+ templateUrl:'template/typeahead/typeahead-popup.html',
3242
+ link:function (scope, element, attrs) {
3243
+
3244
+ scope.templateUrl = attrs.templateUrl;
3245
+
3246
+ scope.isOpen = function () {
3247
+ return scope.matches.length > 0;
3248
+ };
3249
+
3250
+ scope.isActive = function (matchIdx) {
3251
+ return scope.active == matchIdx;
3252
+ };
3253
+
3254
+ scope.selectActive = function (matchIdx) {
3255
+ scope.active = matchIdx;
3256
+ };
3257
+
3258
+ scope.selectMatch = function (activeIdx) {
3259
+ scope.select({activeIdx:activeIdx});
3260
+ };
3261
+ }
3262
+ };
3263
+ })
3264
+
3265
+ .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) {
3266
+ return {
3267
+ restrict:'EA',
3268
+ scope:{
3269
+ index:'=',
3270
+ match:'=',
3271
+ query:'='
3272
+ },
3273
+ link:function (scope, element, attrs) {
3274
+ var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
3275
+ $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
3276
+ element.replaceWith($compile(tplContent.trim())(scope));
3277
+ });
3278
+ }
3279
+ };
3280
+ }])
3281
+
3282
+ .filter('typeaheadHighlight', function() {
3283
+
3284
+ function escapeRegexp(queryToEscape) {
3285
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
3286
+ }
3287
+
3288
+ return function(matchItem, query) {
3289
+ return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
3290
+ };
3291
+ });