├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── angular-panel-snap.js ├── angular-panel-snap.min.js ├── angular-panel-snap.min.js.map ├── bower.json ├── example ├── index.html ├── script.js └── styles.css ├── gulpfile.js ├── package.json ├── readme.md ├── src ├── menu.js ├── module.js ├── panel-group.js ├── panel.js └── scroll.js └── test ├── karma.conf.js └── unit └── panelsnap.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | npm-debug.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.2.0](https://github.com/akreitals/angular-panel-snap/tree/0.2.0) (2015-05-17) 4 | ### Bug Fixes 5 | * removed manual transclusion with specified scope ([#2](https://github.com/akreitals/angular-panel-snap/issues/2)) 6 | * updated for Angular 1.3.x 7 | 8 | ### Breaking Changes 9 | * `enableSnap`, `disableSnap` and `toggleSnap` must now be referenced on `ak-panel`'s parent scope. Example: 10 | 11 | ``` 12 | 13 | ``` 14 | 15 | * `ng-controller` directive cannot be added alongside `ak-panel`, Angular 1.3 `ng-transclude` changes will create competing isolate scopes and produce [multidir](https://docs.angularjs.org/error/$compile/multidir) error. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Adam Kreitals 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /angular-panel-snap.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /* 5 | * angular-panel-snap main module definition 6 | */ 7 | angular.module('akreitals.panel-snap', []); 8 | 9 | })(); 10 | 11 | (function() { 12 | 'use strict'; 13 | 14 | /* 15 | * ak-panel-group-menu directive 16 | * 17 | * Creates a menu for the referenced ak-panel-group container 18 | * 19 | * @attribute for (required) String: name attribute of the ak-panel-group the menu is to reference 20 | */ 21 | angular 22 | .module('akreitals.panel-snap') 23 | .directive('akPanelGroupMenu', akPanelGroupMenu); 24 | 25 | /* @ngInject */ 26 | function akPanelGroupMenu ($rootScope, $log) { 27 | return { 28 | restrict: 'EA', 29 | template: '', 30 | scope: { 31 | for: '@' 32 | }, 33 | link: function (scope) { 34 | if (!angular.isDefined(scope.for)) { 35 | $log.error("PanelGroupMenu: no 'for' attribute provided"); 36 | return; 37 | } 38 | 39 | scope.panels = []; 40 | 41 | /* 42 | * listen for addedPanel event, if group name matches then add 43 | * it to the menu 44 | */ 45 | $rootScope.$on('panelsnap:addedPanel', function (event, data) { 46 | if (scope.for === data.group) { 47 | event.stopPropagation(); 48 | var panel = { 49 | id: data.id, 50 | name: data.name, 51 | active: false 52 | }; 53 | scope.panels.push(panel); 54 | } 55 | }); 56 | 57 | /* 58 | * listen for activatePanel event, if group name matches then set 59 | * active flag target menu element 60 | */ 61 | $rootScope.$on('panelsnap:activatePanel', function (event, data) { 62 | if (scope.for === data.group) { 63 | event.stopPropagation(); 64 | angular.forEach(scope.panels, function (panel) { 65 | panel.active = false; 66 | }); 67 | scope.panels[data.id].active = true; 68 | } 69 | }); 70 | 71 | /* 72 | * emit event to tell ak-panel-group directive to select the target panel 73 | */ 74 | scope.select = function (id) { 75 | $rootScope.$emit('panelsnap:selectPanel', {group: scope.for, id: id}); 76 | }; 77 | } 78 | }; 79 | } 80 | akPanelGroupMenu.$inject = ["$rootScope", "$log"]; 81 | 82 | })(); 83 | 84 | (function() { 85 | 'use strict'; 86 | 87 | /* 88 | * ak-panel-group directive 89 | * 90 | * Container for set of 'ak-panel' directives that maintains the panels state and all interactions with the group 91 | * 92 | * @attribute name (optional) String: name of the group, to be referenced in ak-panel-group-menu's 'for' attribute 93 | * @attribute speed (optional) Number: duration in milliseconds to snap to the desired panel, defaults to 400ms 94 | * @attribute threshold (optional) Number: amount of pixels required to scroll before snapping to the next panel, defults to 50px 95 | * @attribute fullWindow (optional) Boolean: true if the panels are to fill the full browser window 96 | * @attribute keyboard (optional) Boolean: true if key presses can be used to navigate panels 97 | * @attribute prevKey (optional) Number: keyCode of key to navigate to previous panel, defaults to 38 (up arrow) 98 | * @attribute nextKey (optional) Number: keyCode of key to navigate to next panel, defaults to 40 (down arrow) 99 | */ 100 | angular 101 | .module('akreitals.panel-snap') 102 | .directive('akPanelGroup', akPanelGroup) 103 | .controller('PanelGroupController', panelGroupController); 104 | 105 | /* @ngInject */ 106 | function akPanelGroup () { 107 | return { 108 | restrict: 'EA', 109 | replace: true, 110 | controller: 'PanelGroupController', 111 | scope: { 112 | name: '@', 113 | speed: '=', 114 | threshold: '=', 115 | fullWindow: '=', 116 | keyboard: '=', 117 | prevKey: '=', 118 | nextKey: '=' 119 | }, 120 | link: function (scope) { 121 | // Call init after child panels have registered with the controller 122 | scope.init(); 123 | } 124 | }; 125 | } 126 | 127 | /* @ngInject */ 128 | function panelGroupController ($scope, $element, $attrs, $window, $timeout, $document, $rootScope) { 129 | var ctrl = this; 130 | 131 | var resizeTimeout; 132 | var scrollTimeout; 133 | 134 | ctrl.panels = []; 135 | 136 | ctrl.currentPanel = 0; 137 | ctrl.scrollInterval = 0; 138 | ctrl.scrollOffset = 0; 139 | ctrl.isSnapping = false; 140 | ctrl.enabled = true; 141 | 142 | ctrl.speed = 400; // default snap animation duration in milliseconds 143 | ctrl.threshold = 50; // default pixel threshold for snap to occur in pixels 144 | ctrl.prevKey = 38; // default prevKey key code - up arrow 145 | ctrl.nextKey = 40; // default nextKey key code - down arrow 146 | 147 | /* 148 | * add a panels scope to the panels array 149 | * - attached to `this` so it can be called from child panel directives 150 | */ 151 | ctrl.addPanel = function (panelScope) { 152 | var panelName = angular.isDefined(panelScope.name) ? panelScope.name : 'Panel ' + (ctrl.panels.length + 1); 153 | ctrl.panels.push(panelScope); 154 | if (angular.isDefined($scope.name)) { 155 | $rootScope.$emit('panelsnap:addedPanel', { group: $scope.name, name: panelName, id: ctrl.panels.length-1 }); 156 | } 157 | }; 158 | 159 | /* 160 | * enable snapping 161 | */ 162 | ctrl.enableSnap = function () { 163 | // TODO: should this snap to closest panel when enabled? 164 | ctrl.enabled = true; 165 | }; 166 | 167 | /* 168 | * disable snapping 169 | */ 170 | ctrl.disableSnap = function () { 171 | ctrl.enabled = false; 172 | }; 173 | 174 | /* 175 | * toggle snapping 176 | */ 177 | ctrl.toggleSnap = function () { 178 | ctrl.enabled = !ctrl.enabled; 179 | }; 180 | 181 | /* 182 | * initialise the controller state 183 | * - called from the directive link function. This ensures it is called after any child panels 184 | * link function has called addPanel and therefore the panels array is filled and valid. 185 | */ 186 | $scope.init = function () { 187 | ctrl.container = $element; 188 | ctrl.eventContainer = ctrl.container; 189 | ctrl.snapContainer = ctrl.container; 190 | 191 | // if full window, bind and snap using document instead of element 192 | if ($scope.fullWindow) { 193 | ctrl.container = angular.element($document[0].documentElement); 194 | ctrl.eventContainer = ctrl.snapContainer = $document; 195 | } 196 | 197 | // set options / variables 198 | ctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight; 199 | ctrl.speed = angular.isDefined($scope.speed) ? $scope.speed : ctrl.speed; 200 | ctrl.threshold = angular.isDefined($scope.threshold) ? $scope.threshold : ctrl.threshold; 201 | ctrl.prevKey = angular.isDefined($scope.prevKey) ? $scope.prevKey : ctrl.prevKey; 202 | ctrl.nextKey = angular.isDefined($scope.nextKey) ? $scope.nextKey : ctrl.nextKey; 203 | 204 | bind(); 205 | activatePanel(ctrl.currentPanel); 206 | }; 207 | 208 | /* 209 | * listen for selectPanel event, if group name matches then snap 210 | * to the target panel 211 | */ 212 | $rootScope.$on('panelsnap:selectPanel', function (event, data) { 213 | if ($scope.name === data.group) { 214 | event.stopPropagation(); 215 | snapToPanel(data.id); 216 | } 217 | }); 218 | 219 | function bind() { 220 | // bind scrolling events 221 | ctrl.eventContainer.on('mousewheel scroll touchmove', scrollFn); 222 | 223 | // bind resize event 224 | angular.element($window).on('resize', resize); 225 | 226 | // bind keyboard events 227 | if ($scope.keyboard) { 228 | angular.element($window).on('keydown', keydown); 229 | } 230 | } 231 | 232 | function keydown(e) { 233 | if (!ctrl.enabled) { 234 | return; 235 | } 236 | 237 | // prevent any keypress events while snapping 238 | if (ctrl.isSnapping) { 239 | if (e.which === ctrl.prevKey || e.which === ctrl.nextKey) { 240 | e.preventDefault(); 241 | return false; 242 | } 243 | return; 244 | } 245 | 246 | switch (e.which) { 247 | case ctrl.prevKey: 248 | e.preventDefault(); 249 | snapToPanel(ctrl.currentPanel - 1); 250 | break; 251 | case ctrl.nextKey: 252 | e.preventDefault(); 253 | snapToPanel(ctrl.currentPanel + 1); 254 | break; 255 | } 256 | } 257 | 258 | function scrollFn(e) { 259 | var threshold = 50; 260 | $timeout.cancel(scrollTimeout); 261 | scrollTimeout = $timeout(function () { 262 | scrollStop(e); 263 | }, threshold); 264 | } 265 | 266 | function resize() { 267 | var threshold = 150; 268 | $timeout.cancel(resizeTimeout); 269 | resizeTimeout = $timeout(function () { 270 | ctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight; 271 | 272 | if (!ctrl.enabled) { 273 | return; 274 | } 275 | 276 | // snap back to current panel after resizing 277 | snapToPanel(ctrl.currentPanel); 278 | }, threshold); 279 | } 280 | 281 | function scrollStop(e) { 282 | e.stopPropagation(); 283 | 284 | // if (ctrl.isMouseDown) { 285 | // return; 286 | // } 287 | 288 | if (ctrl.isSnapping) { 289 | return; 290 | } 291 | 292 | var target; 293 | var offset = ctrl.snapContainer.scrollTop(); 294 | 295 | if (!ctrl.enabled) { 296 | // still want to activate the correct panel even if snapping is disabled 297 | target = Math.max(0, Math.min(Math.round(offset / ctrl.scrollInterval), ctrl.panels.length - 1)); 298 | if (target !== ctrl.currentPanel) { 299 | activatePanel(target); 300 | } 301 | return; 302 | } 303 | 304 | var scrollDifference = offset - ctrl.scrollOffset; 305 | var maxOffset = ctrl.container[0].scrollHeight - ctrl.scrollInterval; 306 | 307 | // determine target panel 308 | if (scrollDifference < -ctrl.threshold && scrollDifference > -ctrl.scrollInterval) { 309 | target = Math.floor(offset / ctrl.scrollInterval); 310 | } else if (scrollDifference > ctrl.threshold && scrollDifference < ctrl.scrollInterval) { 311 | target = Math.ceil(offset / ctrl.scrollInterval); 312 | } else { 313 | target = Math.round(offset / ctrl.scrollInterval); 314 | } 315 | 316 | // ensure target is within panel array bounds 317 | target = Math.max(0, Math.min(target, ctrl.panels.length - 1)); 318 | 319 | if (scrollDifference === 0) { 320 | // Do nothing 321 | } else if (offset <= 0 || offset >= maxOffset) { 322 | // only activate to prevent stuttering 323 | activatePanel(target); 324 | // set a scrollOffset to a sane number for next scroll 325 | ctrl.scrollOffset = offset <= 0 ? 0 : maxOffset; 326 | } else { 327 | snapToPanel(target); 328 | } 329 | } 330 | 331 | function snapToPanel(target) { 332 | if (isNaN(target) || target < 0 || target >= ctrl.panels.length) { 333 | return; 334 | } 335 | 336 | ctrl.isSnapping = true; 337 | 338 | $rootScope.$broadcast('panelsnap:start', { group: $scope.name }); 339 | ctrl.panels[ctrl.currentPanel].onLeave(); 340 | 341 | var scrollTarget = ctrl.scrollInterval * target; 342 | ctrl.snapContainer.scrollTo(0, scrollTarget, ctrl.speed).then(function () { 343 | ctrl.scrollOffset = scrollTarget; 344 | ctrl.isSnapping = false; 345 | 346 | $rootScope.$broadcast('panelsnap:finish', { group: $scope.name }); 347 | ctrl.panels[target].onEnter(); 348 | 349 | activatePanel(target); 350 | }); 351 | } 352 | 353 | function activatePanel(target) { 354 | // if no panels, or panels have not yet loaded (within ng-repeat) return 355 | if (!ctrl.panels || ctrl.panels.length < 1) { 356 | return; 357 | } 358 | 359 | angular.forEach(ctrl.panels, function (panel) { 360 | panel.setActive(false); 361 | }); 362 | ctrl.panels[target].setActive(true); 363 | ctrl.currentPanel = target; 364 | 365 | // TODO: call onActivate function for target 366 | $rootScope.$broadcast('panelsnap:activate', {group: $scope.name }); 367 | $rootScope.$emit('panelsnap:activatePanel', { group: $scope.name, id: target }); 368 | } 369 | } 370 | panelGroupController.$inject = ["$scope", "$element", "$attrs", "$window", "$timeout", "$document", "$rootScope"]; 371 | 372 | 373 | })(); 374 | 375 | (function() { 376 | 'use strict'; 377 | 378 | /* 379 | * ak-panel directive 380 | * 381 | * Creates a panel inside an ak-panel-group directive. Must be a child of an ak-panel-group element. 382 | * 383 | * @attribute name (optional) String: name of panel, will form text of nav element in any ak-panel-group-menu's assocaited with the containing group 384 | * @attribute onEnter (optional) Function: function to be called when panel is snapped into 385 | * @attribute onLeave (optional) Function: function to be called when panel is snapped out of 386 | */ 387 | angular 388 | .module('akreitals.panel-snap') 389 | .directive('akPanel', akPanel); 390 | 391 | /* @ngInject */ 392 | function akPanel () { 393 | return { 394 | restrict: 'EA', 395 | require: '^akPanelGroup', 396 | replace: true, 397 | transclude: true, 398 | scope: { 399 | name: '@', 400 | onEnter: '&', 401 | onLeave: '&' 402 | }, 403 | template: '
', 404 | link: function (scope, element, attrs, ctrl) { 405 | 406 | // add to parent ak-panel-group 407 | ctrl.addPanel(scope); 408 | 409 | // default panel styles 410 | element.css({ 411 | 'width': '100%', 412 | 'height': '100%', 413 | 'position': 'relative', 414 | 'overflow': 'hidden' 415 | }); 416 | 417 | // attach enable/disable scroll methods to scope - need be accessed by $parent due to transclude scope 418 | scope.enableSnap = ctrl.enableSnap; 419 | scope.disableSnap = ctrl.disableSnap; 420 | scope.toggleSnap = ctrl.toggleSnap; 421 | 422 | // active flag and getter function, to set class .active on panel 423 | scope.active = false; 424 | scope.setActive = function (active) { 425 | scope.active = active; 426 | }; 427 | } 428 | }; 429 | } 430 | 431 | })(); 432 | 433 | (function() { 434 | 'use strict'; 435 | 436 | /* 437 | * Scroll methods - removes the need for external jQuery or GreenSock libraries 438 | * 439 | * Adapted from durated's Angular Scroll module 440 | * https://github.com/durated/angular-scroll 441 | */ 442 | angular 443 | .module('akreitals.panel-snap') 444 | .value('scrollEasing', scrollEasing) 445 | .run(runFn) 446 | .factory('polyfill', polyfill) 447 | .factory('requestAnimation', requestAnimation) 448 | .factory('cancelAnimation', cancelAnimation); 449 | 450 | function scrollEasing (x) { 451 | if(x < 0.5) { 452 | return Math.pow(x*2, 2)/2; 453 | } 454 | return 1-Math.pow((1-x)*2, 2)/2; 455 | } 456 | 457 | /* @ngInject */ 458 | function runFn ($window, $q, cancelAnimation, requestAnimation, scrollEasing) { 459 | var proto = angular.element.prototype; 460 | 461 | var isDocument = function(el) { 462 | return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE); 463 | }; 464 | 465 | var isElement = function(el) { 466 | return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE); 467 | }; 468 | 469 | var unwrap = function(el) { 470 | return isElement(el) || isDocument(el) ? el : el[0]; 471 | }; 472 | 473 | proto.scrollTo = function(left, top, duration) { 474 | var aliasFn; 475 | if(angular.isElement(left)) { 476 | aliasFn = this.scrollToElement; 477 | } else if(duration) { 478 | aliasFn = this.scrollToAnimated; 479 | } 480 | if(aliasFn) { 481 | return aliasFn.apply(this, arguments); 482 | } 483 | var el = unwrap(this); 484 | if(isDocument(el)) { 485 | return $window.scrollTo(left, top); 486 | } 487 | el.scrollLeft = left; 488 | el.scrollTop = top; 489 | }; 490 | 491 | proto.scrollToAnimated = function(left, top, duration, easing) { 492 | var scrollAnimation, deferred; 493 | if(duration && !easing) { 494 | easing = scrollEasing; 495 | } 496 | var startLeft = this.scrollLeft(), 497 | startTop = this.scrollTop(), 498 | deltaLeft = Math.round(left - startLeft), 499 | deltaTop = Math.round(top - startTop); 500 | 501 | var startTime = null; 502 | var el = this; 503 | 504 | var cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown'; 505 | var cancelScrollAnimation = function($event) { 506 | if (!$event || $event.which > 0) { 507 | el.unbind(cancelOnEvents, cancelScrollAnimation); 508 | cancelAnimation(scrollAnimation); 509 | deferred.reject(); 510 | scrollAnimation = null; 511 | } 512 | }; 513 | 514 | if(scrollAnimation) { 515 | cancelScrollAnimation(); 516 | } 517 | deferred = $q.defer(); 518 | 519 | if(!deltaLeft && !deltaTop) { 520 | deferred.resolve(); 521 | return deferred.promise; 522 | } 523 | 524 | var animationStep = function(timestamp) { 525 | if (startTime === null) { 526 | startTime = timestamp; 527 | } 528 | 529 | var progress = timestamp - startTime; 530 | var percent = (progress >= duration ? 1 : easing(progress/duration)); 531 | 532 | el.scrollTo( 533 | startLeft + Math.ceil(deltaLeft * percent), 534 | startTop + Math.ceil(deltaTop * percent) 535 | ); 536 | if(percent < 1) { 537 | scrollAnimation = requestAnimation(animationStep); 538 | } else { 539 | el.unbind(cancelOnEvents, cancelScrollAnimation); 540 | scrollAnimation = null; 541 | deferred.resolve(); 542 | } 543 | }; 544 | 545 | //Fix random mobile safari bug when scrolling to top by hitting status bar 546 | el.scrollTo(startLeft, startTop); 547 | 548 | // el.bind(cancelOnEvents, cancelScrollAnimation); 549 | 550 | scrollAnimation = requestAnimation(animationStep); 551 | return deferred.promise; 552 | }; 553 | 554 | proto.scrollToElement = function(target, offset, duration, easing) { 555 | var el = unwrap(this); 556 | var top = this.scrollTop() + unwrap(target).getBoundingClientRect().top - (offset || 0); 557 | if(isElement(el)) { 558 | top -= el.getBoundingClientRect().top; 559 | } 560 | return this.scrollTo(0, top, duration, easing); 561 | }; 562 | 563 | var overloaders = { 564 | scrollLeft: function(value, duration, easing) { 565 | if(angular.isNumber(value)) { 566 | return this.scrollTo(value, this.scrollTop(), duration, easing); 567 | } 568 | var el = unwrap(this); 569 | if(isDocument(el)) { 570 | return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft; 571 | } 572 | return el.scrollLeft; 573 | }, 574 | scrollTop: function(value, duration, easing) { 575 | if(angular.isNumber(value)) { 576 | return this.scrollTo(this.scrollTop(), value, duration, easing); 577 | } 578 | var el = unwrap(this); 579 | if(isDocument(el)) { 580 | return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; 581 | } 582 | return el.scrollTop; 583 | } 584 | }; 585 | 586 | //Add duration and easing functionality to existing jQuery getter/setters 587 | var overloadScrollPos = function(superFn, overloadFn) { 588 | return function(value, duration) { 589 | if(duration) { 590 | return overloadFn.apply(this, arguments); 591 | } 592 | return superFn.apply(this, arguments); 593 | }; 594 | }; 595 | 596 | for(var methodName in overloaders) { 597 | proto[methodName] = (proto[methodName] ? overloadScrollPos(proto[methodName], overloaders[methodName]) : overloaders[methodName]); 598 | } 599 | } 600 | runFn.$inject = ["$window", "$q", "cancelAnimation", "requestAnimation", "scrollEasing"]; 601 | 602 | /* @ngInject */ 603 | function polyfill ($window) { 604 | var vendors = ['webkit', 'moz', 'o', 'ms']; 605 | 606 | return function(fnName, fallback) { 607 | if($window[fnName]) { 608 | return $window[fnName]; 609 | } 610 | var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1); 611 | for(var key, i = 0; i < vendors.length; i++) { 612 | key = vendors[i]+suffix; 613 | if($window[key]) { 614 | return $window[key]; 615 | } 616 | } 617 | return fallback; 618 | }; 619 | } 620 | polyfill.$inject = ["$window"]; 621 | 622 | /* @ngInject */ 623 | function requestAnimation (polyfill, $timeout) { 624 | var lastTime = 0; 625 | var fallback = function(callback) { 626 | var currTime = new Date().getTime(); 627 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 628 | var id = $timeout(function() { 629 | callback(currTime + timeToCall); 630 | }, timeToCall); 631 | lastTime = currTime + timeToCall; 632 | return id; 633 | }; 634 | 635 | return polyfill('requestAnimationFrame', fallback); 636 | } 637 | requestAnimation.$inject = ["polyfill", "$timeout"]; 638 | 639 | /* @ngInject */ 640 | function cancelAnimation (polyfill, $timeout) { 641 | var fallback = function(promise) { 642 | $timeout.cancel(promise); 643 | }; 644 | 645 | return polyfill('cancelAnimationFrame', fallback); 646 | } 647 | cancelAnimation.$inject = ["polyfill", "$timeout"]; 648 | 649 | })(); 650 | -------------------------------------------------------------------------------- /angular-panel-snap.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";angular.module("akreitals.panel-snap",[])}(),function(){"use strict";function e(e,n){return{restrict:"EA",template:'',scope:{"for":"@"},link:function(t){return angular.isDefined(t["for"])?(t.panels=[],e.$on("panelsnap:addedPanel",function(e,n){if(t["for"]===n.group){e.stopPropagation();var a={id:n.id,name:n.name,active:!1};t.panels.push(a)}}),e.$on("panelsnap:activatePanel",function(e,n){t["for"]===n.group&&(e.stopPropagation(),angular.forEach(t.panels,function(e){e.active=!1}),t.panels[n.id].active=!0)}),void(t.select=function(n){e.$emit("panelsnap:selectPanel",{group:t["for"],id:n})})):void n.error("PanelGroupMenu: no 'for' attribute provided")}}}angular.module("akreitals.panel-snap").directive("akPanelGroupMenu",e),e.$inject=["$rootScope","$log"]}(),function(){"use strict";function e(){return{restrict:"EA",replace:!0,controller:"PanelGroupController",scope:{name:"@",speed:"=",threshold:"=",fullWindow:"=",keyboard:"=",prevKey:"=",nextKey:"="},link:function(e){e.init()}}}function n(e,n,t,a,r,l,o){function i(){m.eventContainer.on("mousewheel scroll touchmove",s),angular.element(a).on("resize",u),e.keyboard&&angular.element(a).on("keydown",c)}function c(e){if(m.enabled)if(m.isSnapping){if(e.which===m.prevKey||e.which===m.nextKey)return e.preventDefault(),!1}else switch(e.which){case m.prevKey:e.preventDefault(),f(m.currentPanel-1);break;case m.nextKey:e.preventDefault(),f(m.currentPanel+1)}}function s(e){var n=50;r.cancel(v),v=r(function(){p(e)},n)}function u(){var e=150;r.cancel(h),h=r(function(){m.scrollInterval=isNaN(m.container[0].innerHeight)?m.container[0].clientHeight:m.container[0].innerHeight,m.enabled&&f(m.currentPanel)},e)}function p(e){if(e.stopPropagation(),!m.isSnapping){var n,t=m.snapContainer.scrollTop();if(!m.enabled)return n=Math.max(0,Math.min(Math.round(t/m.scrollInterval),m.panels.length-1)),void(n!==m.currentPanel&&d(n));var a=t-m.scrollOffset,r=m.container[0].scrollHeight-m.scrollInterval;n=a<-m.threshold&&a>-m.scrollInterval?Math.floor(t/m.scrollInterval):a>m.threshold&&a=t||t>=r?(d(n),m.scrollOffset=0>=t?0:r):f(n))}}function f(n){if(!(isNaN(n)||0>n||n>=m.panels.length)){m.isSnapping=!0,o.$broadcast("panelsnap:start",{group:e.name}),m.panels[m.currentPanel].onLeave();var t=m.scrollInterval*n;m.snapContainer.scrollTo(0,t,m.speed).then(function(){m.scrollOffset=t,m.isSnapping=!1,o.$broadcast("panelsnap:finish",{group:e.name}),m.panels[n].onEnter(),d(n)})}}function d(n){!m.panels||m.panels.length<1||(angular.forEach(m.panels,function(e){e.setActive(!1)}),m.panels[n].setActive(!0),m.currentPanel=n,o.$broadcast("panelsnap:activate",{group:e.name}),o.$emit("panelsnap:activatePanel",{group:e.name,id:n}))}var h,v,m=this;m.panels=[],m.currentPanel=0,m.scrollInterval=0,m.scrollOffset=0,m.isSnapping=!1,m.enabled=!0,m.speed=400,m.threshold=50,m.prevKey=38,m.nextKey=40,m.addPanel=function(n){var t=angular.isDefined(n.name)?n.name:"Panel "+(m.panels.length+1);m.panels.push(n),angular.isDefined(e.name)&&o.$emit("panelsnap:addedPanel",{group:e.name,name:t,id:m.panels.length-1})},m.enableSnap=function(){m.enabled=!0},m.disableSnap=function(){m.enabled=!1},m.toggleSnap=function(){m.enabled=!m.enabled},e.init=function(){m.container=n,m.eventContainer=m.container,m.snapContainer=m.container,e.fullWindow&&(m.container=angular.element(l[0].documentElement),m.eventContainer=m.snapContainer=l),m.scrollInterval=isNaN(m.container[0].innerHeight)?m.container[0].clientHeight:m.container[0].innerHeight,m.speed=angular.isDefined(e.speed)?e.speed:m.speed,m.threshold=angular.isDefined(e.threshold)?e.threshold:m.threshold,m.prevKey=angular.isDefined(e.prevKey)?e.prevKey:m.prevKey,m.nextKey=angular.isDefined(e.nextKey)?e.nextKey:m.nextKey,i(),d(m.currentPanel)},o.$on("panelsnap:selectPanel",function(n,t){e.name===t.group&&(n.stopPropagation(),f(t.id))})}angular.module("akreitals.panel-snap").directive("akPanelGroup",e).controller("PanelGroupController",n),n.$inject=["$scope","$element","$attrs","$window","$timeout","$document","$rootScope"]}(),function(){"use strict";function e(){return{restrict:"EA",require:"^akPanelGroup",replace:!0,transclude:!0,scope:{name:"@",onEnter:"&",onLeave:"&"},template:'
',link:function(e,n,t,a){a.addPanel(e),n.css({width:"100%",height:"100%",position:"relative",overflow:"hidden"}),e.enableSnap=a.enableSnap,e.disableSnap=a.disableSnap,e.toggleSnap=a.toggleSnap,e.active=!1,e.setActive=function(n){e.active=n}}}}angular.module("akreitals.panel-snap").directive("akPanel",e)}(),function(){"use strict";function e(e){return.5>e?Math.pow(2*e,2)/2:1-Math.pow(2*(1-e),2)/2}function n(e,n,t,a,r){var l=angular.element.prototype,o=function(e){return"undefined"!=typeof HTMLDocument&&e instanceof HTMLDocument||e.nodeType&&e.nodeType===e.DOCUMENT_NODE},i=function(e){return"undefined"!=typeof HTMLElement&&e instanceof HTMLElement||e.nodeType&&e.nodeType===e.ELEMENT_NODE},c=function(e){return i(e)||o(e)?e:e[0]};l.scrollTo=function(n,t,a){var r;if(angular.isElement(n)?r=this.scrollToElement:a&&(r=this.scrollToAnimated),r)return r.apply(this,arguments);var l=c(this);return o(l)?e.scrollTo(n,t):(l.scrollLeft=n,void(l.scrollTop=t))},l.scrollToAnimated=function(e,l,o,i){var c,s;o&&!i&&(i=r);var u=this.scrollLeft(),p=this.scrollTop(),f=Math.round(e-u),d=Math.round(l-p),h=null,v=this,m="scroll mousedown mousewheel touchmove keydown",g=function(e){(!e||e.which>0)&&(v.unbind(m,g),t(c),s.reject(),c=null)};if(c&&g(),s=n.defer(),!f&&!d)return s.resolve(),s.promise;var y=function(e){null===h&&(h=e);var n=e-h,t=n>=o?1:i(n/o);v.scrollTo(u+Math.ceil(f*t),p+Math.ceil(d*t)),1>t?c=a(y):(v.unbind(m,g),c=null,s.resolve())};return v.scrollTo(u,p),c=a(y),s.promise},l.scrollToElement=function(e,n,t,a){var r=c(this),l=this.scrollTop()+c(e).getBoundingClientRect().top-(n||0);return i(r)&&(l-=r.getBoundingClientRect().top),this.scrollTo(0,l,t,a)};var s={scrollLeft:function(n,t,a){if(angular.isNumber(n))return this.scrollTo(n,this.scrollTop(),t,a);var r=c(this);return o(r)?e.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft:r.scrollLeft},scrollTop:function(n,t,a){if(angular.isNumber(n))return this.scrollTo(this.scrollTop(),n,t,a);var r=c(this);return o(r)?e.scrollY||document.documentElement.scrollTop||document.body.scrollTop:r.scrollTop}},u=function(e,n){return function(t,a){return a?n.apply(this,arguments):e.apply(this,arguments)}};for(var p in s)l[p]=l[p]?u(l[p],s[p]):s[p]}function t(e){var n=["webkit","moz","o","ms"];return function(t,a){if(e[t])return e[t];for(var r,l=t.substr(0,1).toUpperCase()+t.substr(1),o=0;o
  • {{panel.name}}
  • ',\n\t\tscope: {\n\t\t\tfor: '@'\n\t\t},\n\t\tlink: function (scope) {\n\t\t\tif (!angular.isDefined(scope.for)) {\n\t\t\t\t$log.error(\"PanelGroupMenu: no 'for' attribute provided\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tscope.panels = [];\n\n\t\t\t/*\n\t\t\t * listen for addedPanel event, if group name matches then add\n\t\t\t * it to the menu\n\t\t\t */\n\t\t\t$rootScope.$on('panelsnap:addedPanel', function (event, data) {\n\t\t\t\tif (scope.for === data.group) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\tvar panel = {\n\t\t\t\t\t\tid: data.id,\n\t\t\t\t\t\tname: data.name,\n\t\t\t\t\t\tactive: false\n\t\t\t\t\t};\n\t\t\t\t\tscope.panels.push(panel);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t/*\n\t\t\t * listen for activatePanel event, if group name matches then set\n\t\t\t * active flag target menu element\n\t\t\t */\n\t\t\t$rootScope.$on('panelsnap:activatePanel', function (event, data) {\n\t\t\t\tif (scope.for === data.group) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\tangular.forEach(scope.panels, function (panel) {\n\t\t\t\t\t\tpanel.active = false;\n\t\t\t\t\t});\n\t\t\t\t\tscope.panels[data.id].active = true;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t/*\n\t\t\t * emit event to tell ak-panel-group directive to select the target panel\n\t\t\t */\n\t\t\tscope.select = function (id) {\n\t\t\t\t$rootScope.$emit('panelsnap:selectPanel', {group: scope.for, id: id});\n\t\t\t};\n\t\t}\n\t};\n}\n\n})();\n",null,"(function() {\n'use strict';\n\n/*\n * ak-panel-group directive\n *\n * Container for set of 'ak-panel' directives that maintains the panels state and all interactions with the group\n *\n * @attribute name (optional) String: name of the group, to be referenced in ak-panel-group-menu's 'for' attribute\n * @attribute speed (optional) Number: duration in milliseconds to snap to the desired panel, defaults to 400ms\n * @attribute threshold (optional) Number: amount of pixels required to scroll before snapping to the next panel, defults to 50px \n * @attribute fullWindow (optional) Boolean: true if the panels are to fill the full browser window\n * @attribute keyboard (optional) Boolean: true if key presses can be used to navigate panels\n * @attribute prevKey (optional) Number: keyCode of key to navigate to previous panel, defaults to 38 (up arrow)\n * @attribute nextKey (optional) Number: keyCode of key to navigate to next panel, defaults to 40 (down arrow)\n */\nangular\n\t.module('akreitals.panel-snap')\n\t.directive('akPanelGroup', akPanelGroup)\n\t.controller('PanelGroupController', panelGroupController);\n\n/* @ngInject */\nfunction akPanelGroup () {\n\treturn {\n\t\trestrict: 'EA',\n\t\treplace: true,\n\t\tcontroller: 'PanelGroupController',\n\t\tscope: {\n\t\t\tname: '@',\n\t\t\tspeed: '=',\n\t\t\tthreshold: '=',\n\t\t\tfullWindow: '=',\n\t\t\tkeyboard: '=',\n\t\t\tprevKey: '=',\n\t\t\tnextKey: '='\n\t\t},\n\t\tlink: function (scope) {\n\t\t\t// Call init after child panels have registered with the controller\n\t\t\tscope.init();\n\t\t}\n\t};\n}\n\n/* @ngInject */\nfunction panelGroupController ($scope, $element, $attrs, $window, $timeout, $document, $rootScope) {\n\tvar ctrl = this;\n\n\tvar resizeTimeout;\n\tvar scrollTimeout;\n\n\tctrl.panels = [];\n\n\tctrl.currentPanel = 0;\n\tctrl.scrollInterval = 0;\n\tctrl.scrollOffset = 0;\n\tctrl.isSnapping = false;\n\tctrl.enabled = true;\n\n\tctrl.speed = 400;\t\t// default snap animation duration in milliseconds\n\tctrl.threshold = 50;\t// default pixel threshold for snap to occur in pixels\n\tctrl.prevKey = 38;\t\t// default prevKey key code - up arrow\n\tctrl.nextKey = 40;\t\t// default nextKey key code - down arrow\n\n\t/*\n\t * add a panels scope to the panels array\n\t * - attached to `this` so it can be called from child panel directives\n\t */\n\tctrl.addPanel = function (panelScope) {\n\t\tvar panelName = angular.isDefined(panelScope.name) ? panelScope.name : 'Panel ' + (ctrl.panels.length + 1);\n\t\tctrl.panels.push(panelScope);\n\t\tif (angular.isDefined($scope.name)) {\n\t\t\t$rootScope.$emit('panelsnap:addedPanel', { group: $scope.name, name: panelName, id: ctrl.panels.length-1 });\n\t\t}\n\t};\n\n\t/*\n\t * enable snapping\n\t */\n\tctrl.enableSnap = function () {\n\t\t// TODO: should this snap to closest panel when enabled?\n\t\tctrl.enabled = true;\n\t};\n\n\t/*\n\t * disable snapping\n\t */\n\tctrl.disableSnap = function () {\n\t\tctrl.enabled = false;\n\t};\n\n\t/*\n\t * toggle snapping\n\t */\n\tctrl.toggleSnap = function () {\n\t\tctrl.enabled = !ctrl.enabled;\n\t};\n\n\t/*\n\t * initialise the controller state\n\t * - called from the directive link function. This ensures it is called after any child panels\n\t * link function has called addPanel and therefore the panels array is filled and valid.\n\t */\n\t$scope.init = function () {\n\t\tctrl.container = $element;\n\t\tctrl.eventContainer = ctrl.container;\n\t\tctrl.snapContainer = ctrl.container;\n\n\t\t// if full window, bind and snap using document instead of element\n\t\tif ($scope.fullWindow) {\n\t\t\tctrl.container = angular.element($document[0].documentElement);\n\t\t\tctrl.eventContainer = ctrl.snapContainer = $document;\n\t\t}\n\n\t\t// set options / variables\n\t\tctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight;\n\t\tctrl.speed = angular.isDefined($scope.speed) ? $scope.speed : ctrl.speed;\n\t\tctrl.threshold = angular.isDefined($scope.threshold) ? $scope.threshold : ctrl.threshold;\n\t\tctrl.prevKey = angular.isDefined($scope.prevKey) ? $scope.prevKey : ctrl.prevKey;\n\t\tctrl.nextKey = angular.isDefined($scope.nextKey) ? $scope.nextKey : ctrl.nextKey;\n\n\t\tbind();\n\t\tactivatePanel(ctrl.currentPanel);\n\t};\n\n\t/*\n\t * listen for selectPanel event, if group name matches then snap\n\t * to the target panel\n\t */\n\t$rootScope.$on('panelsnap:selectPanel', function (event, data) {\n\t\tif ($scope.name === data.group) {\n\t\t\tevent.stopPropagation();\n\t\t\tsnapToPanel(data.id);\n\t\t}\n\t});\n\n\tfunction bind() {\n\t\t// bind scrolling events\n\t\tctrl.eventContainer.on('mousewheel scroll touchmove', scrollFn);\n\n\t\t// bind resize event\n\t\tangular.element($window).on('resize', resize);\n\n\t\t// bind keyboard events\n\t\tif ($scope.keyboard) {\n\t\t\tangular.element($window).on('keydown', keydown);\n\t\t}\n\t}\n\n\tfunction keydown(e) {\n\t\tif (!ctrl.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\t// prevent any keypress events while snapping\n\t\tif (ctrl.isSnapping) {\n\t\t\tif (e.which === ctrl.prevKey || e.which === ctrl.nextKey) {\n\t\t\t\te.preventDefault();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (e.which) {\n\t\t\tcase ctrl.prevKey:\n\t\t\t\te.preventDefault();\n\t\t\t\tsnapToPanel(ctrl.currentPanel - 1);\n\t\t\t\tbreak;\n\t\t\tcase ctrl.nextKey:\n\t\t\t\te.preventDefault();\n\t\t\t\tsnapToPanel(ctrl.currentPanel + 1);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tfunction scrollFn(e) {\n\t\tvar threshold = 50;\n\t\t$timeout.cancel(scrollTimeout);\n\t\tscrollTimeout = $timeout(function () {\n\t\t\tscrollStop(e);\n\t\t}, threshold);\n\t}\n\n\tfunction resize() {\n\t\tvar threshold = 150;\n\t\t$timeout.cancel(resizeTimeout);\n\t\tresizeTimeout = $timeout(function () {\n\t\t\tctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight;\n\t\t\t\n\t\t\tif (!ctrl.enabled) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// snap back to current panel after resizing\n\t\t\tsnapToPanel(ctrl.currentPanel);\n\t\t}, threshold);\n\t}\n\n\tfunction scrollStop(e) {\n\t\te.stopPropagation();\n\n\t\t// if (ctrl.isMouseDown) {\n\t\t// \treturn;\n\t\t// }\n\n\t\tif (ctrl.isSnapping) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar target;\n\t\tvar offset = ctrl.snapContainer.scrollTop();\n\n\t\tif (!ctrl.enabled) {\n\t\t\t// still want to activate the correct panel even if snapping is disabled\n\t\t\ttarget = Math.max(0, Math.min(Math.round(offset / ctrl.scrollInterval), ctrl.panels.length - 1));\n\t\t\tif (target !== ctrl.currentPanel) {\n\t\t\t\tactivatePanel(target);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tvar scrollDifference = offset - ctrl.scrollOffset;\n\t\tvar maxOffset = ctrl.container[0].scrollHeight - ctrl.scrollInterval;\n\n\t\t// determine target panel\n\t\tif (scrollDifference < -ctrl.threshold && scrollDifference > -ctrl.scrollInterval) {\n\t\t\ttarget = Math.floor(offset / ctrl.scrollInterval);\n\t\t} else if (scrollDifference > ctrl.threshold && scrollDifference < ctrl.scrollInterval) {\n\t\t\ttarget = Math.ceil(offset / ctrl.scrollInterval);\n\t\t} else {\n\t\t\ttarget = Math.round(offset / ctrl.scrollInterval);\n\t\t}\n\n\t\t// ensure target is within panel array bounds\n\t\ttarget = Math.max(0, Math.min(target, ctrl.panels.length - 1));\n\n\t\tif (scrollDifference === 0) {\n\t\t\t// Do nothing\n\t\t} else if (offset <= 0 || offset >= maxOffset) {\n\t\t\t// only activate to prevent stuttering\n\t\t\tactivatePanel(target);\n\t\t\t// set a scrollOffset to a sane number for next scroll\n\t\t\tctrl.scrollOffset = offset <= 0 ? 0 : maxOffset;\n\t\t} else {\n\t\t\tsnapToPanel(target);\n\t\t}\n\t}\n\n\tfunction snapToPanel(target) {\n\t\tif (isNaN(target) || target < 0 || target >= ctrl.panels.length) {\n\t\t\treturn;\n\t\t}\n\n\t\tctrl.isSnapping = true;\n\n\t\t$rootScope.$broadcast('panelsnap:start', { group: $scope.name });\n\t\tctrl.panels[ctrl.currentPanel].onLeave();\n\n\t\tvar scrollTarget = ctrl.scrollInterval * target;\n\t\tctrl.snapContainer.scrollTo(0, scrollTarget, ctrl.speed).then(function () {\n\t\t\tctrl.scrollOffset = scrollTarget;\n\t\t\tctrl.isSnapping = false;\n\n\t\t\t$rootScope.$broadcast('panelsnap:finish', { group: $scope.name });\n\t\t\tctrl.panels[target].onEnter();\n\n\t\t\tactivatePanel(target);\n\t\t});\n\t}\n\n\tfunction activatePanel(target) {\n\t\t// if no panels, or panels have not yet loaded (within ng-repeat) return\n\t\tif (!ctrl.panels || ctrl.panels.length < 1) { \n\t\t\treturn;\n\t\t}\n\n\t\tangular.forEach(ctrl.panels, function (panel) {\n\t\t\tpanel.setActive(false);\n\t\t});\n\t\tctrl.panels[target].setActive(true);\n\t\tctrl.currentPanel = target;\n\n\t\t// TODO: call onActivate function for target\n\t\t$rootScope.$broadcast('panelsnap:activate', {group: $scope.name });\n\t\t$rootScope.$emit('panelsnap:activatePanel', { group: $scope.name, id: target });\n\t}\n}\n\n\n})();\n","(function() {\n'use strict';\n\n/*\n * ak-panel directive\n *\n * Creates a panel inside an ak-panel-group directive. Must be a child of an ak-panel-group element.\n *\n * @attribute name (optional) String: name of panel, will form text of nav element in any ak-panel-group-menu's assocaited with the containing group\n * @attribute onEnter (optional) Function: function to be called when panel is snapped into\n * @attribute onLeave (optional) Function: function to be called when panel is snapped out of\n */\nangular\n\t.module('akreitals.panel-snap')\n\t.directive('akPanel', akPanel);\n\n/* @ngInject */\nfunction akPanel () {\n\treturn {\n\t\trestrict: 'EA',\n\t\trequire: '^akPanelGroup',\n\t\treplace: true,\n\t\ttransclude: true,\n\t\tscope: {\n\t\t\tname: '@',\n\t\t\tonEnter: '&',\n\t\t\tonLeave: '&'\n\t\t},\n\t\ttemplate: '
    ',\n\t\tlink: function (scope, element, attrs, ctrl) {\n\n\t\t\t// add to parent ak-panel-group\n\t\t\tctrl.addPanel(scope);\n\n\t\t\t// default panel styles\n\t\t\telement.css({\n\t\t\t\t'width': '100%',\n\t\t\t\t'height': '100%',\n\t\t\t\t'position': 'relative',\n\t\t\t\t'overflow': 'hidden'\n\t\t\t});\n\n\t\t\t// attach enable/disable scroll methods to scope - need be accessed by $parent due to transclude scope\n\t\t\tscope.enableSnap = ctrl.enableSnap;\n\t\t\tscope.disableSnap = ctrl.disableSnap;\n\t\t\tscope.toggleSnap = ctrl.toggleSnap;\n\n\t\t\t// active flag and getter function, to set class .active on panel\n\t\t\tscope.active = false;\n\t\t\tscope.setActive = function (active) {\n\t\t\t\tscope.active = active;\n\t\t\t};\n\t\t}\n\t};\n}\n\n})();\n","(function() {\n'use strict';\n\n/*\n * Scroll methods - removes the need for external jQuery or GreenSock libraries\n *\n * Adapted from durated's Angular Scroll module\n * https://github.com/durated/angular-scroll\n */\nangular\n\t.module('akreitals.panel-snap')\n\t.value('scrollEasing', scrollEasing)\n\t.run(runFn)\n\t.factory('polyfill', polyfill)\n\t.factory('requestAnimation', requestAnimation)\n\t.factory('cancelAnimation', cancelAnimation);\n\nfunction scrollEasing (x) {\n\tif(x < 0.5) {\n\t\treturn Math.pow(x*2, 2)/2;\n\t}\n\treturn 1-Math.pow((1-x)*2, 2)/2;\n}\n\n/* @ngInject */\nfunction runFn ($window, $q, cancelAnimation, requestAnimation, scrollEasing) {\n\tvar proto = angular.element.prototype;\n\n\tvar isDocument = function(el) {\n\t\treturn (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE);\n\t};\n\n\tvar isElement = function(el) {\n\t\treturn (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE);\n\t};\n\n\tvar unwrap = function(el) {\n\t\treturn isElement(el) || isDocument(el) ? el : el[0];\n\t};\n\n\tproto.scrollTo = function(left, top, duration) {\n\t\tvar aliasFn;\n\t\tif(angular.isElement(left)) {\n\t\t\taliasFn = this.scrollToElement;\n\t\t} else if(duration) {\n\t\t\taliasFn = this.scrollToAnimated;\n\t\t}\n\t\tif(aliasFn) {\n\t\t\treturn aliasFn.apply(this, arguments);\n\t\t}\n\t\tvar el = unwrap(this);\n\t\tif(isDocument(el)) {\n\t\t\treturn $window.scrollTo(left, top);\n\t\t}\n\t\tel.scrollLeft = left;\n\t\tel.scrollTop = top;\n\t};\n\n\tproto.scrollToAnimated = function(left, top, duration, easing) {\n\t\tvar scrollAnimation, deferred;\n\t\tif(duration && !easing) {\n\t\t\teasing = scrollEasing;\n\t\t}\n\t\tvar startLeft = this.scrollLeft(),\n\t\t\tstartTop = this.scrollTop(),\n\t\t\tdeltaLeft = Math.round(left - startLeft),\n\t\t\tdeltaTop = Math.round(top - startTop);\n\n\t\tvar startTime = null;\n\t\tvar el = this;\n\n\t\tvar cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown';\n\t\tvar cancelScrollAnimation = function($event) {\n\t\t\tif (!$event || $event.which > 0) {\n\t\t\t\tel.unbind(cancelOnEvents, cancelScrollAnimation);\n\t\t\t\tcancelAnimation(scrollAnimation);\n\t\t\t\tdeferred.reject();\n\t\t\t\tscrollAnimation = null;\n\t\t\t}\n\t\t};\n\n\t\tif(scrollAnimation) {\n\t\t\tcancelScrollAnimation();\n\t\t}\n\t\tdeferred = $q.defer();\n\n\t\tif(!deltaLeft && !deltaTop) {\n\t\t\tdeferred.resolve();\n\t\t\treturn deferred.promise;\n\t\t}\n\n\t\tvar animationStep = function(timestamp) {\n\t\t\tif (startTime === null) {\n\t\t\t\tstartTime = timestamp;\n\t\t\t}\n\n\t\t\tvar progress = timestamp - startTime;\n\t\t\tvar percent = (progress >= duration ? 1 : easing(progress/duration));\n\n\t\t\tel.scrollTo(\n\t\t\t\tstartLeft + Math.ceil(deltaLeft * percent),\n\t\t\t\tstartTop + Math.ceil(deltaTop * percent)\n\t\t\t);\n\t\t\tif(percent < 1) {\n\t\t\t\tscrollAnimation = requestAnimation(animationStep);\n\t\t\t} else {\n\t\t\t\tel.unbind(cancelOnEvents, cancelScrollAnimation);\n\t\t\t\tscrollAnimation = null;\n\t\t\t\tdeferred.resolve();\n\t\t\t}\n\t\t};\n\n\t\t//Fix random mobile safari bug when scrolling to top by hitting status bar\n\t\tel.scrollTo(startLeft, startTop);\n\n\t\t// el.bind(cancelOnEvents, cancelScrollAnimation);\n\n\t\tscrollAnimation = requestAnimation(animationStep);\n\t\treturn deferred.promise;\n\t};\n\n\tproto.scrollToElement = function(target, offset, duration, easing) {\n\t\tvar el = unwrap(this);\n\t\tvar top = this.scrollTop() + unwrap(target).getBoundingClientRect().top - (offset || 0);\n\t\tif(isElement(el)) {\n\t\t\ttop -= el.getBoundingClientRect().top;\n\t\t}\n\t\treturn this.scrollTo(0, top, duration, easing);\n\t};\n\n\tvar overloaders = {\n\t\tscrollLeft: function(value, duration, easing) {\n\t\t\tif(angular.isNumber(value)) {\n\t\t\t\treturn this.scrollTo(value, this.scrollTop(), duration, easing);\n\t\t\t}\n\t\t\tvar el = unwrap(this);\n\t\t\tif(isDocument(el)) {\n\t\t\t\treturn $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;\n\t\t\t}\n\t\t\treturn el.scrollLeft;\n\t\t},\n\t\tscrollTop: function(value, duration, easing) {\n\t\t\tif(angular.isNumber(value)) {\n\t\t\t\treturn this.scrollTo(this.scrollTop(), value, duration, easing);\n\t\t\t}\n\t\t\tvar el = unwrap(this);\n\t\t\tif(isDocument(el)) {\n\t\t\t\treturn $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;\n\t\t\t}\n\t\t\treturn el.scrollTop;\n\t\t}\n\t};\n\n\t//Add duration and easing functionality to existing jQuery getter/setters\n\tvar overloadScrollPos = function(superFn, overloadFn) {\n\t\treturn function(value, duration) {\n\t\t\tif(duration) {\n\t\t\t\treturn overloadFn.apply(this, arguments);\n\t\t\t}\n\t\t\treturn superFn.apply(this, arguments);\n\t\t};\n\t};\n\n\tfor(var methodName in overloaders) {\n\t\tproto[methodName] = (proto[methodName] ? overloadScrollPos(proto[methodName], overloaders[methodName]) : overloaders[methodName]);\n\t}\n}\n\n/* @ngInject */\nfunction polyfill ($window) {\n\tvar vendors = ['webkit', 'moz', 'o', 'ms'];\n\n\treturn function(fnName, fallback) {\n\t\tif($window[fnName]) {\n\t\t\treturn $window[fnName];\n\t\t}\n\t\tvar suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);\n\t\tfor(var key, i = 0; i < vendors.length; i++) {\n\t\t\tkey = vendors[i]+suffix;\n\t\t\tif($window[key]) {\n\t\t\t\treturn $window[key];\n\t\t\t}\n\t\t}\n\t\treturn fallback;\n\t};\n}\n\n/* @ngInject */\nfunction requestAnimation (polyfill, $timeout) {\n\tvar lastTime = 0;\n\tvar fallback = function(callback) {\n\t\tvar currTime = new Date().getTime();\n\t\tvar timeToCall = Math.max(0, 16 - (currTime - lastTime));\n\t\tvar id = $timeout(function() {\n\t\t\tcallback(currTime + timeToCall);\n\t\t}, timeToCall);\n\t\tlastTime = currTime + timeToCall;\n\t\treturn id;\n\t};\n\n\treturn polyfill('requestAnimationFrame', fallback);\n}\n\n/* @ngInject */\nfunction cancelAnimation (polyfill, $timeout) {\n\tvar fallback = function(promise) {\n\t\t$timeout.cancel(promise);\n\t};\n\n\treturn polyfill('cancelAnimationFrame', fallback);\n}\n\n})();\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-panel-snap", 3 | "version": "0.2.0", 4 | "description": "AngularJS module to provide snapping functionality to scrolling between panels", 5 | "main": "angular-panel-snap.min.js", 6 | "keywords": [ 7 | "angular", 8 | "angularjs", 9 | "panel", 10 | "snap", 11 | "scroll", 12 | "window" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests", 21 | "package.json", 22 | "src", 23 | "example", 24 | "gulpfile.js" 25 | ], 26 | "dependencies": { 27 | "angular": "~1.3.15" 28 | }, 29 | "devDependencies": { 30 | "angular-mocks": "~1.3.15" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular-Panel-Snap example 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 |

    Angular-Panel-Snap

    16 |

    A set of AngularJS directives that will snap to panels (blocks) of content after scrolling. Panels can also be nested and combined with a navigation directive as shown below.

    17 |

    The following panels demonstrate some of the module's features.

    18 |

    http://github.com/akreitals/angular-panel-snap

    19 |
    20 |
    21 | Scroll down to see more. 22 |
    23 |
    24 | 25 | 26 |

    Navigation menu

    27 |

    Simply add an ak-panel-group-menu directive to add a dynamic navigation menu.

    28 | 29 | 30 | 31 |

    Panel 1

    32 | {{pants.msg}} 33 |

    You can disable the scroll, the menu will still be functional

    34 | 35 | 36 | 37 |
    38 |

    Panel 2

    39 |

    Panel 3

    40 |

    Panel 4

    41 |
    42 |
    43 | 44 | 45 |

    Keyboard navigation

    46 |

    Use the left and right arrow keys to navigate this panel group (keys are customisable).

    47 | 48 |

    Panel 1

    49 |

    Panel 2

    50 |

    Panel 3

    51 |

    Panel 4

    52 |
    53 |
    54 | 55 | 56 |

    Events

    57 |

    Add functions to the onEnter or onLeave attributes or bind to panelsnap events to trigger custom actions and animations. 58 |

    59 | 				{{main.textLog}}
    60 | 			
    61 | 62 |

    Panel 1

    63 |

    Panel 2

    64 |

    Panel 3

    65 |

    Panel 4

    66 |
    67 |
    68 |
    69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /example/script.js: -------------------------------------------------------------------------------- 1 | angular.module('app', ['akreitals.panel-snap']) 2 | 3 | .controller('MainCtrl', function ($rootScope) { 4 | var vm = this; 5 | 6 | // initialise log for event handlers 7 | vm.textLog = "\n"; 8 | 9 | vm.enterFn = function () { 10 | vm.show = true; 11 | vm.textLog += "* Events Panel Entered\n"; 12 | }; 13 | vm.leaveFn = function () { 14 | vm.show = false; 15 | vm.textLog += "* Events Panel Left\n"; 16 | }; 17 | 18 | $rootScope.$on('panelsnap:start', function (event, data) { 19 | if (data.group === "eventPanelGroup") 20 | { 21 | vm.textLog += " - Subgroup start snapping\n"; 22 | } 23 | }); 24 | 25 | $rootScope.$on('panelsnap:finish', function (event, data) { 26 | if (data.group === "eventPanelGroup") 27 | { 28 | vm.textLog += " - Subgroup finish snapping\n"; 29 | } 30 | }); 31 | 32 | $rootScope.$on('panelsnap:activate', function (event, data) { 33 | if (data.group === "eventPanelGroup") 34 | { 35 | vm.textLog += " - Subgroup panel activated\n"; 36 | } 37 | }); 38 | }); -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Lato:100); 2 | 3 | html, body { 4 | margin: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | body { 9 | font-family: 'Lato'; 10 | font-weight: 100; 11 | font-size: 100%; 12 | color: #ffffff; 13 | background: #f1c40f; 14 | } 15 | h1 { 16 | text-transform: uppercase; 17 | font-size: 4.5em; 18 | margin: 0 0 20px 0; 19 | } 20 | a { 21 | color: inherit; 22 | text-decoration: none; 23 | border-bottom: 1px solid #ffffff; 24 | padding-bottom: 3px; 25 | } 26 | p { 27 | font-size: 1.5em; /* ~24px */ 28 | } 29 | ul { 30 | padding: 0; 31 | margin: 0; 32 | } 33 | li { 34 | list-style: none; 35 | } 36 | .ak-panel { 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | -ms-box-sizing: border-box; 40 | -o-box-sizing: border-box; 41 | box-sizing: border-box; 42 | } 43 | .main-group { 44 | width: 100%; 45 | height: 100%; 46 | } 47 | .main-group .ak-panel { 48 | background: #1abc9c; 49 | padding: 20px; 50 | } 51 | .main-group .ak-panel:nth-child(2n) { 52 | background: #16a085; 53 | } 54 | 55 | .sub-group { 56 | position: absolute; 57 | top: 200px; 58 | bottom: 50px; 59 | right: 50px; 60 | left: 400px; 61 | overflow-y: scroll; 62 | } 63 | .sub-group .ak-panel { 64 | background: #f1c40f; 65 | } 66 | .sub-group .ak-panel:nth-child(2n) { 67 | background: #f39c12; 68 | } 69 | 70 | .sub-group.keyboard { 71 | left: 150px; 72 | right: 150px; 73 | } 74 | 75 | .ak-panel.introduction { 76 | text-align: center; 77 | display: table; 78 | background: none; 79 | } 80 | .ak-panel.introduction:after { 81 | content: ''; 82 | display: block; 83 | position: absolute; 84 | top: -200%; 85 | left: -150%; 86 | height: 500%; 87 | width: 200%; 88 | background: #f39c12; 89 | z-index: -1; 90 | 91 | -moz-transform-origin: 100% 50%; 92 | -o-transform-origin: 100% 50%; 93 | -webkit-transform-origin: 100% 50%; 94 | transform-origin: 100% 50%; 95 | 96 | -moz-transform: rotate(-45deg); 97 | -o-transform: rotate(-45deg); 98 | -webkit-transform: rotate(-45deg); 99 | transform: rotate(-45deg); 100 | } 101 | 102 | .ak-panel.introduction .center { 103 | display: table-cell; 104 | vertical-align: middle; 105 | } 106 | 107 | .ak-panel.introduction .center p { 108 | margin-left: auto; 109 | margin-right: auto; 110 | max-width: 900px; 111 | } 112 | 113 | .ak-panel.introduction .bottom { 114 | position: absolute; 115 | bottom: 0; 116 | left: 0; 117 | right: 0; 118 | padding-bottom: 50px; 119 | 120 | text-align: center; 121 | font-size: 60%; 122 | } 123 | .ak-panel.introduction .bottom:after { 124 | content: ''; 125 | display: block; 126 | margin: 1em auto 0; 127 | height: 20px; 128 | width: 20px; 129 | border-right: 1px solid; 130 | border-bottom: 1px solid; 131 | 132 | -moz-transform: rotate(45deg); 133 | -o-transform: rotate(45deg); 134 | -webkit-transform: rotate(45deg); 135 | transform: rotate(45deg); 136 | } 137 | 138 | .ak-menu, .event-feed { 139 | position: absolute; 140 | top: 200px; 141 | bottom: 50px; 142 | right: auto; 143 | left: 50px; 144 | width: 300px; 145 | } 146 | .ak-menu a { 147 | display: block; 148 | padding: 25px; 149 | background: #f1c40f; 150 | margin: 0 0 25px 0; 151 | border: none; 152 | } 153 | .ak-menu .active a, 154 | .ak-menu a:active, 155 | .ak-menu a:hover { 156 | background: #f39c12; 157 | } 158 | .event-feed { 159 | overflow-y: scroll; 160 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | // load plugins 4 | var del = require('del'); 5 | var join = require('path').join; 6 | var karma = require('karma').server; 7 | var plugins = require('gulp-load-plugins')(); 8 | 9 | // load tests config 10 | var karmaConfigPath = './test/karma.conf.js'; 11 | var karmaConf = require(karmaConfigPath); 12 | 13 | // globs and options 14 | var sources = ['src/**/module.js', 'src/**/*.js']; 15 | var targets = 'angular-panel-snap.{js,min.js,min.js.map}'; 16 | var taskOptions = { 17 | jshint: { 18 | eqeqeq: true, 19 | camelcase: true, 20 | freeze: true, 21 | immed: true, 22 | // latedef: true, 23 | newcap: true, 24 | undef: true, 25 | unused: true, 26 | browser: true, 27 | globals: { 28 | angular: false, 29 | console: false, 30 | HTMLDocument: false 31 | } 32 | }, 33 | karma: { 34 | configFile: karmaConfigPath 35 | } 36 | } 37 | 38 | // error function 39 | var onError = function (err) { 40 | plugins.util.beep(); 41 | plugins.util.log(err); 42 | }; 43 | 44 | gulp.task('default', ['lint', 'test', 'clean', 'compile']); 45 | 46 | gulp.task('watch', ['default'], function () { 47 | gulp.watch(sources, ['lint', 'test', 'compile']); 48 | gulp.watch(karmaConf.testFiles, ['test']); 49 | }); 50 | 51 | gulp.task('clean', function () { 52 | del(targets); 53 | }); 54 | 55 | gulp.task('lint', function () { 56 | gulp.src(sources) 57 | .pipe(plugins.jshint(taskOptions.jshint)) 58 | .pipe(plugins.jshint.reporter('default')); 59 | }); 60 | 61 | gulp.task('test', function (cb) { 62 | karma.start({ 63 | configFile: join(__dirname, taskOptions.karma.configFile), 64 | singleRun: true 65 | }, cb); 66 | }); 67 | 68 | gulp.task('compile', function () { 69 | gulp.src(sources) 70 | .pipe(plugins.plumber({ 71 | errorHandler: onError 72 | })) 73 | .pipe(plugins.wrap('(function() {\n\'use strict\';\n\n<%= contents %>\n\n})();\n\n')) 74 | .pipe(plugins.sourcemaps.init()) 75 | .pipe(plugins.concat('angular-panel-snap.js')) 76 | .pipe(plugins.ngAnnotate()) 77 | .pipe(gulp.dest('./')) 78 | .pipe(plugins.rename({ 79 | suffix: '.min' 80 | })) 81 | .pipe(plugins.uglify({ 82 | mangle: true 83 | })) 84 | .pipe(plugins.sourcemaps.write('./')) 85 | .pipe(gulp.dest('./')); 86 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-panel-snap", 3 | "version": "0.2.0", 4 | "description": "AngularJS module that allows panels to be added to markup hat will be snapped-to on scroll", 5 | "keywords": [ 6 | "angular", 7 | "panel", 8 | "snap", 9 | "scroll", 10 | "window" 11 | ], 12 | "main": "angular-panel-snap.js", 13 | "directories": { 14 | "example": "example", 15 | "test": "tests" 16 | }, 17 | "scripts": { 18 | "build": "gulp", 19 | "test": "gulp test" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://git@github.com:akreitals/angular-panel-snap.git" 24 | }, 25 | "author": "akreitals (kreitals@hotmail.com)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/akreitals/angular-panel-snap/issues" 29 | }, 30 | "homepage": "https://github.com/akreitals/angular-panel-snap", 31 | "devDependencies": { 32 | "del": "^1.1.1", 33 | "gulp": "^3.8.11", 34 | "gulp-concat": "^2.5.2", 35 | "gulp-jshint": "^1.7.1", 36 | "gulp-load-plugins": "^0.10.0", 37 | "gulp-ng-annotate": "^0.5.3", 38 | "gulp-plumber": "^1.0.0", 39 | "gulp-rename": "^1.2.2", 40 | "gulp-sourcemaps": "^1.5.2", 41 | "gulp-uglify": "^1.2.0", 42 | "gulp-util": "^3.0.4", 43 | "gulp-wrap": "^0.11.0", 44 | "jasmine-core": "^2.3.4", 45 | "karma": "^0.12.31", 46 | "karma-chrome-launcher": "^0.1.12", 47 | "karma-jasmine": "^0.3.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #angular-panel-snap 2 | An AngularJS module that provides scroll snapping functionality and menu's to sets of panels within a page. 3 | 4 | Only dependent on AngularJS (no jQuery or additional animation libraries required). Based on [jQuery.panelSnap](https://github.com/guidobouman/jquery-panelsnap). 5 | 6 | ---- 7 | 8 | ##Demo 9 | Check out the live [demo](http://akreitals.github.io/angular-panel-snap) 10 | 11 | ##Installation 12 | Download [angular-panel-snap.js](https://raw.github.com/akreitals/master/angular-panel-snap.js)([minified version](https://raw.github.com/akreitals/master/angular-panel-snap.js)) and place it with your other scripts. Alternatively you can use Bower: 13 | 14 | $ bower install angular-panel-snap 15 | 16 | Include the script in your application: 17 | 18 | 19 | 20 | And remember to add angular-panel-snap as a dependency to your application module: 21 | 22 | angular.module('myApplicationModule', ['akreitals.panel-snap']); 23 | 24 | ##Usage 25 | ###Basic usage 26 | A simple group of panels: 27 | ```html 28 | 29 | 30 |

    First Panel

    31 |

    First panel content

    32 |
    33 | 34 | 35 |

    Second Panel

    36 |

    Second panel content

    37 |
    38 | 39 | 40 |

    Third Panel

    41 |

    Third panel content

    42 |
    43 |
    44 | ``` 45 | To include full page (full browser window) panels, like the main panel group in the [demo](http://akreitals.github.io/angular-panel-snap), the `full-window` attribute must be set to true. This ensures that the scroll bindings listen to events on the `document` object instead of the parent container element. 46 | 47 | Please note in order for panels to correctly display the width and height of a containing element must be defined. 48 | 49 | For full window panels you should ensure your stylesheet contains something similar to the following: 50 | 51 | ```css 52 | html, body { 53 | margin: 0; 54 | width: 100%; 55 | height: 100% 56 | } 57 | ``` 58 | 59 | ###Nested panel groups 60 | Panel groups can be nested: 61 | ```html 62 | 63 | 64 |

    First Panel

    65 |

    First panel content

    66 |
    67 | 68 | 69 |

    Second panel

    70 |

    Second panel content

    71 | 72 | 73 | 74 |

    Nested panel one

    75 |
    76 | 77 | 78 |

    Nested panel two

    79 |
    80 |
    81 |
    82 |
    83 | ``` 84 | 85 | ###Panel group menu 86 | A dynamic navigation menu can be easily added to any panel group provided it's `name` attribute is set: 87 | ```html 88 | 89 | 90 | 91 |

    First Panel

    92 |

    First panel content

    93 |
    94 | 95 | 96 |

    Second Panel

    97 |

    Second panel content

    98 |
    99 | 100 | 101 |

    Third Panel

    102 |

    Third panel content

    103 |
    104 |
    105 | ``` 106 | 107 | ##Directives 108 | All the options for the various directives are summarised in the tables below. 109 | ### ak-panel-group 110 | Container for set of `ak-panel` directives that maintains the panels state and all interactions with the group. 111 | 112 | | Attr | Type | Details | 113 | | ---- | ---- | ------- | 114 | | name (optional) | String | name of the group, to be referenced in ak-panel-group-menu's 'for' attribute | 115 | | speed (optional) | Number | duration in milliseconds to snap to the desired panel, defaults to 400ms | 116 | | threshold (optional) | Number | amount of pixels required to scroll before snapping to the next panel, defults to 50px | 117 | | fullWindow (optional) | Boolean | true if the panels are to fill the full browser window | 118 | | keyboard (optional) | Boolean | true if key presses can be used to navigate panels | 119 | | prevKey (optional) | Number | keyCode of key to navigate to previous panel, defaults to 38 (up arrow) | 120 | | nextKey (optional) | Number | keyCode of key to navigate to next panel, defaults to 40 (down arrow) | 121 | 122 | ### ak-panel 123 | Creates a panel inside an `ak-panel-group` directive. Must be a child of an `ak-panel-group` element. 124 | 125 | | Attr | Type | Details | 126 | | ---- | ---- | ------- | 127 | | name (optional) | String | name of panel, will form text of nav element in any ak-panel-group-menu's assocaited with the containing group | 128 | | onEnter (optional) | Function | function to be called when panel is snapped into | 129 | | onLeave (optional) | Function | function to be called when panel is snapped out of | 130 | 131 | ### ak-panel-menu 132 | Creates a menu for the referenced `ak-panel-group` container. 133 | 134 | | Attr | Type | Details | 135 | | ---- | ---- | ------- | 136 | | for (required) | String | name attribute of the ak-panel-group the menu is to reference | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ak-panel-group-menu directive 3 | * 4 | * Creates a menu for the referenced ak-panel-group container 5 | * 6 | * @attribute for (required) String: name attribute of the ak-panel-group the menu is to reference 7 | */ 8 | angular 9 | .module('akreitals.panel-snap') 10 | .directive('akPanelGroupMenu', akPanelGroupMenu); 11 | 12 | /* @ngInject */ 13 | function akPanelGroupMenu ($rootScope, $log) { 14 | return { 15 | restrict: 'EA', 16 | template: '', 17 | scope: { 18 | for: '@' 19 | }, 20 | link: function (scope) { 21 | if (!angular.isDefined(scope.for)) { 22 | $log.error("PanelGroupMenu: no 'for' attribute provided"); 23 | return; 24 | } 25 | 26 | scope.panels = []; 27 | 28 | /* 29 | * listen for addedPanel event, if group name matches then add 30 | * it to the menu 31 | */ 32 | $rootScope.$on('panelsnap:addedPanel', function (event, data) { 33 | if (scope.for === data.group) { 34 | event.stopPropagation(); 35 | var panel = { 36 | id: data.id, 37 | name: data.name, 38 | active: false 39 | }; 40 | scope.panels.push(panel); 41 | } 42 | }); 43 | 44 | /* 45 | * listen for activatePanel event, if group name matches then set 46 | * active flag target menu element 47 | */ 48 | $rootScope.$on('panelsnap:activatePanel', function (event, data) { 49 | if (scope.for === data.group) { 50 | event.stopPropagation(); 51 | angular.forEach(scope.panels, function (panel) { 52 | panel.active = false; 53 | }); 54 | scope.panels[data.id].active = true; 55 | } 56 | }); 57 | 58 | /* 59 | * emit event to tell ak-panel-group directive to select the target panel 60 | */ 61 | scope.select = function (id) { 62 | $rootScope.$emit('panelsnap:selectPanel', {group: scope.for, id: id}); 63 | }; 64 | } 65 | }; 66 | } -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-panel-snap main module definition 3 | */ 4 | angular.module('akreitals.panel-snap', []); -------------------------------------------------------------------------------- /src/panel-group.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ak-panel-group directive 3 | * 4 | * Container for set of 'ak-panel' directives that maintains the panels state and all interactions with the group 5 | * 6 | * @attribute name (optional) String: name of the group, to be referenced in ak-panel-group-menu's 'for' attribute 7 | * @attribute speed (optional) Number: duration in milliseconds to snap to the desired panel, defaults to 400ms 8 | * @attribute threshold (optional) Number: amount of pixels required to scroll before snapping to the next panel, defults to 50px 9 | * @attribute fullWindow (optional) Boolean: true if the panels are to fill the full browser window 10 | * @attribute keyboard (optional) Boolean: true if key presses can be used to navigate panels 11 | * @attribute prevKey (optional) Number: keyCode of key to navigate to previous panel, defaults to 38 (up arrow) 12 | * @attribute nextKey (optional) Number: keyCode of key to navigate to next panel, defaults to 40 (down arrow) 13 | */ 14 | angular 15 | .module('akreitals.panel-snap') 16 | .directive('akPanelGroup', akPanelGroup) 17 | .controller('PanelGroupController', panelGroupController); 18 | 19 | /* @ngInject */ 20 | function akPanelGroup () { 21 | return { 22 | restrict: 'EA', 23 | replace: true, 24 | controller: 'PanelGroupController', 25 | scope: { 26 | name: '@', 27 | speed: '=', 28 | threshold: '=', 29 | fullWindow: '=', 30 | keyboard: '=', 31 | prevKey: '=', 32 | nextKey: '=' 33 | }, 34 | link: function (scope) { 35 | // Call init after child panels have registered with the controller 36 | scope.init(); 37 | } 38 | }; 39 | } 40 | 41 | /* @ngInject */ 42 | function panelGroupController ($scope, $element, $attrs, $window, $timeout, $document, $rootScope) { 43 | var ctrl = this; 44 | 45 | var resizeTimeout; 46 | var scrollTimeout; 47 | 48 | ctrl.panels = []; 49 | 50 | ctrl.currentPanel = 0; 51 | ctrl.scrollInterval = 0; 52 | ctrl.scrollOffset = 0; 53 | ctrl.isSnapping = false; 54 | ctrl.enabled = true; 55 | 56 | ctrl.speed = 400; // default snap animation duration in milliseconds 57 | ctrl.threshold = 50; // default pixel threshold for snap to occur in pixels 58 | ctrl.prevKey = 38; // default prevKey key code - up arrow 59 | ctrl.nextKey = 40; // default nextKey key code - down arrow 60 | 61 | /* 62 | * add a panels scope to the panels array 63 | * - attached to `this` so it can be called from child panel directives 64 | */ 65 | ctrl.addPanel = function (panelScope) { 66 | var panelName = angular.isDefined(panelScope.name) ? panelScope.name : 'Panel ' + (ctrl.panels.length + 1); 67 | ctrl.panels.push(panelScope); 68 | if (angular.isDefined($scope.name)) { 69 | $rootScope.$emit('panelsnap:addedPanel', { group: $scope.name, name: panelName, id: ctrl.panels.length-1 }); 70 | } 71 | }; 72 | 73 | /* 74 | * enable snapping 75 | */ 76 | ctrl.enableSnap = function () { 77 | // TODO: should this snap to closest panel when enabled? 78 | ctrl.enabled = true; 79 | }; 80 | 81 | /* 82 | * disable snapping 83 | */ 84 | ctrl.disableSnap = function () { 85 | ctrl.enabled = false; 86 | }; 87 | 88 | /* 89 | * toggle snapping 90 | */ 91 | ctrl.toggleSnap = function () { 92 | ctrl.enabled = !ctrl.enabled; 93 | }; 94 | 95 | /* 96 | * initialise the controller state 97 | * - called from the directive link function. This ensures it is called after any child panels 98 | * link function has called addPanel and therefore the panels array is filled and valid. 99 | */ 100 | $scope.init = function () { 101 | ctrl.container = $element; 102 | ctrl.eventContainer = ctrl.container; 103 | ctrl.snapContainer = ctrl.container; 104 | 105 | // if full window, bind and snap using document instead of element 106 | if ($scope.fullWindow) { 107 | ctrl.container = angular.element($document[0].documentElement); 108 | ctrl.eventContainer = ctrl.snapContainer = $document; 109 | } 110 | 111 | // set options / variables 112 | ctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight; 113 | ctrl.speed = angular.isDefined($scope.speed) ? $scope.speed : ctrl.speed; 114 | ctrl.threshold = angular.isDefined($scope.threshold) ? $scope.threshold : ctrl.threshold; 115 | ctrl.prevKey = angular.isDefined($scope.prevKey) ? $scope.prevKey : ctrl.prevKey; 116 | ctrl.nextKey = angular.isDefined($scope.nextKey) ? $scope.nextKey : ctrl.nextKey; 117 | 118 | bind(); 119 | activatePanel(ctrl.currentPanel); 120 | }; 121 | 122 | /* 123 | * listen for selectPanel event, if group name matches then snap 124 | * to the target panel 125 | */ 126 | $rootScope.$on('panelsnap:selectPanel', function (event, data) { 127 | if ($scope.name === data.group) { 128 | event.stopPropagation(); 129 | snapToPanel(data.id); 130 | } 131 | }); 132 | 133 | function bind() { 134 | // bind scrolling events 135 | ctrl.eventContainer.on('mousewheel scroll touchmove', scrollFn); 136 | 137 | // bind resize event 138 | angular.element($window).on('resize', resize); 139 | 140 | // bind keyboard events 141 | if ($scope.keyboard) { 142 | angular.element($window).on('keydown', keydown); 143 | } 144 | } 145 | 146 | function keydown(e) { 147 | if (!ctrl.enabled) { 148 | return; 149 | } 150 | 151 | // prevent any keypress events while snapping 152 | if (ctrl.isSnapping) { 153 | if (e.which === ctrl.prevKey || e.which === ctrl.nextKey) { 154 | e.preventDefault(); 155 | return false; 156 | } 157 | return; 158 | } 159 | 160 | switch (e.which) { 161 | case ctrl.prevKey: 162 | e.preventDefault(); 163 | snapToPanel(ctrl.currentPanel - 1); 164 | break; 165 | case ctrl.nextKey: 166 | e.preventDefault(); 167 | snapToPanel(ctrl.currentPanel + 1); 168 | break; 169 | } 170 | } 171 | 172 | function scrollFn(e) { 173 | var threshold = 50; 174 | $timeout.cancel(scrollTimeout); 175 | scrollTimeout = $timeout(function () { 176 | scrollStop(e); 177 | }, threshold); 178 | } 179 | 180 | function resize() { 181 | var threshold = 150; 182 | $timeout.cancel(resizeTimeout); 183 | resizeTimeout = $timeout(function () { 184 | ctrl.scrollInterval = isNaN(ctrl.container[0].innerHeight) ? ctrl.container[0].clientHeight : ctrl.container[0].innerHeight; 185 | 186 | if (!ctrl.enabled) { 187 | return; 188 | } 189 | 190 | // snap back to current panel after resizing 191 | snapToPanel(ctrl.currentPanel); 192 | }, threshold); 193 | } 194 | 195 | function scrollStop(e) { 196 | e.stopPropagation(); 197 | 198 | // if (ctrl.isMouseDown) { 199 | // return; 200 | // } 201 | 202 | if (ctrl.isSnapping) { 203 | return; 204 | } 205 | 206 | var target; 207 | var offset = ctrl.snapContainer.scrollTop(); 208 | 209 | if (!ctrl.enabled) { 210 | // still want to activate the correct panel even if snapping is disabled 211 | target = Math.max(0, Math.min(Math.round(offset / ctrl.scrollInterval), ctrl.panels.length - 1)); 212 | if (target !== ctrl.currentPanel) { 213 | activatePanel(target); 214 | } 215 | return; 216 | } 217 | 218 | var scrollDifference = offset - ctrl.scrollOffset; 219 | var maxOffset = ctrl.container[0].scrollHeight - ctrl.scrollInterval; 220 | 221 | // determine target panel 222 | if (scrollDifference < -ctrl.threshold && scrollDifference > -ctrl.scrollInterval) { 223 | target = Math.floor(offset / ctrl.scrollInterval); 224 | } else if (scrollDifference > ctrl.threshold && scrollDifference < ctrl.scrollInterval) { 225 | target = Math.ceil(offset / ctrl.scrollInterval); 226 | } else { 227 | target = Math.round(offset / ctrl.scrollInterval); 228 | } 229 | 230 | // ensure target is within panel array bounds 231 | target = Math.max(0, Math.min(target, ctrl.panels.length - 1)); 232 | 233 | if (scrollDifference === 0) { 234 | // Do nothing 235 | } else if (offset <= 0 || offset >= maxOffset) { 236 | // only activate to prevent stuttering 237 | activatePanel(target); 238 | // set a scrollOffset to a sane number for next scroll 239 | ctrl.scrollOffset = offset <= 0 ? 0 : maxOffset; 240 | } else { 241 | snapToPanel(target); 242 | } 243 | } 244 | 245 | function snapToPanel(target) { 246 | if (isNaN(target) || target < 0 || target >= ctrl.panels.length) { 247 | return; 248 | } 249 | 250 | ctrl.isSnapping = true; 251 | 252 | $rootScope.$broadcast('panelsnap:start', { group: $scope.name }); 253 | ctrl.panels[ctrl.currentPanel].onLeave(); 254 | 255 | var scrollTarget = ctrl.scrollInterval * target; 256 | ctrl.snapContainer.scrollTo(0, scrollTarget, ctrl.speed).then(function () { 257 | ctrl.scrollOffset = scrollTarget; 258 | ctrl.isSnapping = false; 259 | 260 | $rootScope.$broadcast('panelsnap:finish', { group: $scope.name }); 261 | ctrl.panels[target].onEnter(); 262 | 263 | activatePanel(target); 264 | }); 265 | } 266 | 267 | function activatePanel(target) { 268 | // if no panels, or panels have not yet loaded (within ng-repeat) return 269 | if (!ctrl.panels || ctrl.panels.length < 1) { 270 | return; 271 | } 272 | 273 | angular.forEach(ctrl.panels, function (panel) { 274 | panel.setActive(false); 275 | }); 276 | ctrl.panels[target].setActive(true); 277 | ctrl.currentPanel = target; 278 | 279 | // TODO: call onActivate function for target 280 | $rootScope.$broadcast('panelsnap:activate', {group: $scope.name }); 281 | $rootScope.$emit('panelsnap:activatePanel', { group: $scope.name, id: target }); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ak-panel directive 3 | * 4 | * Creates a panel inside an ak-panel-group directive. Must be a child of an ak-panel-group element. 5 | * 6 | * @attribute name (optional) String: name of panel, will form text of nav element in any ak-panel-group-menu's assocaited with the containing group 7 | * @attribute onEnter (optional) Function: function to be called when panel is snapped into 8 | * @attribute onLeave (optional) Function: function to be called when panel is snapped out of 9 | */ 10 | angular 11 | .module('akreitals.panel-snap') 12 | .directive('akPanel', akPanel); 13 | 14 | /* @ngInject */ 15 | function akPanel () { 16 | return { 17 | restrict: 'EA', 18 | require: '^akPanelGroup', 19 | replace: true, 20 | transclude: true, 21 | scope: { 22 | name: '@', 23 | onEnter: '&', 24 | onLeave: '&' 25 | }, 26 | template: '
    ', 27 | link: function (scope, element, attrs, ctrl) { 28 | 29 | // add to parent ak-panel-group 30 | ctrl.addPanel(scope); 31 | 32 | // default panel styles 33 | element.css({ 34 | 'width': '100%', 35 | 'height': '100%', 36 | 'position': 'relative', 37 | 'overflow': 'hidden' 38 | }); 39 | 40 | // attach enable/disable scroll methods to scope - need be accessed by $parent due to transclude scope 41 | scope.enableSnap = ctrl.enableSnap; 42 | scope.disableSnap = ctrl.disableSnap; 43 | scope.toggleSnap = ctrl.toggleSnap; 44 | 45 | // active flag and getter function, to set class .active on panel 46 | scope.active = false; 47 | scope.setActive = function (active) { 48 | scope.active = active; 49 | }; 50 | } 51 | }; 52 | } -------------------------------------------------------------------------------- /src/scroll.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Scroll methods - removes the need for external jQuery or GreenSock libraries 3 | * 4 | * Adapted from durated's Angular Scroll module 5 | * https://github.com/durated/angular-scroll 6 | */ 7 | angular 8 | .module('akreitals.panel-snap') 9 | .value('scrollEasing', scrollEasing) 10 | .run(runFn) 11 | .factory('polyfill', polyfill) 12 | .factory('requestAnimation', requestAnimation) 13 | .factory('cancelAnimation', cancelAnimation); 14 | 15 | function scrollEasing (x) { 16 | if(x < 0.5) { 17 | return Math.pow(x*2, 2)/2; 18 | } 19 | return 1-Math.pow((1-x)*2, 2)/2; 20 | } 21 | 22 | /* @ngInject */ 23 | function runFn ($window, $q, cancelAnimation, requestAnimation, scrollEasing) { 24 | var proto = angular.element.prototype; 25 | 26 | var isDocument = function(el) { 27 | return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE); 28 | }; 29 | 30 | var isElement = function(el) { 31 | return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE); 32 | }; 33 | 34 | var unwrap = function(el) { 35 | return isElement(el) || isDocument(el) ? el : el[0]; 36 | }; 37 | 38 | proto.scrollTo = function(left, top, duration) { 39 | var aliasFn; 40 | if(angular.isElement(left)) { 41 | aliasFn = this.scrollToElement; 42 | } else if(duration) { 43 | aliasFn = this.scrollToAnimated; 44 | } 45 | if(aliasFn) { 46 | return aliasFn.apply(this, arguments); 47 | } 48 | var el = unwrap(this); 49 | if(isDocument(el)) { 50 | return $window.scrollTo(left, top); 51 | } 52 | el.scrollLeft = left; 53 | el.scrollTop = top; 54 | }; 55 | 56 | proto.scrollToAnimated = function(left, top, duration, easing) { 57 | var scrollAnimation, deferred; 58 | if(duration && !easing) { 59 | easing = scrollEasing; 60 | } 61 | var startLeft = this.scrollLeft(), 62 | startTop = this.scrollTop(), 63 | deltaLeft = Math.round(left - startLeft), 64 | deltaTop = Math.round(top - startTop); 65 | 66 | var startTime = null; 67 | var el = this; 68 | 69 | var cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown'; 70 | var cancelScrollAnimation = function($event) { 71 | if (!$event || $event.which > 0) { 72 | el.unbind(cancelOnEvents, cancelScrollAnimation); 73 | cancelAnimation(scrollAnimation); 74 | deferred.reject(); 75 | scrollAnimation = null; 76 | } 77 | }; 78 | 79 | if(scrollAnimation) { 80 | cancelScrollAnimation(); 81 | } 82 | deferred = $q.defer(); 83 | 84 | if(!deltaLeft && !deltaTop) { 85 | deferred.resolve(); 86 | return deferred.promise; 87 | } 88 | 89 | var animationStep = function(timestamp) { 90 | if (startTime === null) { 91 | startTime = timestamp; 92 | } 93 | 94 | var progress = timestamp - startTime; 95 | var percent = (progress >= duration ? 1 : easing(progress/duration)); 96 | 97 | el.scrollTo( 98 | startLeft + Math.ceil(deltaLeft * percent), 99 | startTop + Math.ceil(deltaTop * percent) 100 | ); 101 | if(percent < 1) { 102 | scrollAnimation = requestAnimation(animationStep); 103 | } else { 104 | el.unbind(cancelOnEvents, cancelScrollAnimation); 105 | scrollAnimation = null; 106 | deferred.resolve(); 107 | } 108 | }; 109 | 110 | //Fix random mobile safari bug when scrolling to top by hitting status bar 111 | el.scrollTo(startLeft, startTop); 112 | 113 | // el.bind(cancelOnEvents, cancelScrollAnimation); 114 | 115 | scrollAnimation = requestAnimation(animationStep); 116 | return deferred.promise; 117 | }; 118 | 119 | proto.scrollToElement = function(target, offset, duration, easing) { 120 | var el = unwrap(this); 121 | var top = this.scrollTop() + unwrap(target).getBoundingClientRect().top - (offset || 0); 122 | if(isElement(el)) { 123 | top -= el.getBoundingClientRect().top; 124 | } 125 | return this.scrollTo(0, top, duration, easing); 126 | }; 127 | 128 | var overloaders = { 129 | scrollLeft: function(value, duration, easing) { 130 | if(angular.isNumber(value)) { 131 | return this.scrollTo(value, this.scrollTop(), duration, easing); 132 | } 133 | var el = unwrap(this); 134 | if(isDocument(el)) { 135 | return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft; 136 | } 137 | return el.scrollLeft; 138 | }, 139 | scrollTop: function(value, duration, easing) { 140 | if(angular.isNumber(value)) { 141 | return this.scrollTo(this.scrollTop(), value, duration, easing); 142 | } 143 | var el = unwrap(this); 144 | if(isDocument(el)) { 145 | return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; 146 | } 147 | return el.scrollTop; 148 | } 149 | }; 150 | 151 | //Add duration and easing functionality to existing jQuery getter/setters 152 | var overloadScrollPos = function(superFn, overloadFn) { 153 | return function(value, duration) { 154 | if(duration) { 155 | return overloadFn.apply(this, arguments); 156 | } 157 | return superFn.apply(this, arguments); 158 | }; 159 | }; 160 | 161 | for(var methodName in overloaders) { 162 | proto[methodName] = (proto[methodName] ? overloadScrollPos(proto[methodName], overloaders[methodName]) : overloaders[methodName]); 163 | } 164 | } 165 | 166 | /* @ngInject */ 167 | function polyfill ($window) { 168 | var vendors = ['webkit', 'moz', 'o', 'ms']; 169 | 170 | return function(fnName, fallback) { 171 | if($window[fnName]) { 172 | return $window[fnName]; 173 | } 174 | var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1); 175 | for(var key, i = 0; i < vendors.length; i++) { 176 | key = vendors[i]+suffix; 177 | if($window[key]) { 178 | return $window[key]; 179 | } 180 | } 181 | return fallback; 182 | }; 183 | } 184 | 185 | /* @ngInject */ 186 | function requestAnimation (polyfill, $timeout) { 187 | var lastTime = 0; 188 | var fallback = function(callback) { 189 | var currTime = new Date().getTime(); 190 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 191 | var id = $timeout(function() { 192 | callback(currTime + timeToCall); 193 | }, timeToCall); 194 | lastTime = currTime + timeToCall; 195 | return id; 196 | }; 197 | 198 | return polyfill('requestAnimationFrame', fallback); 199 | } 200 | 201 | /* @ngInject */ 202 | function cancelAnimation (polyfill, $timeout) { 203 | var fallback = function(promise) { 204 | $timeout.cancel(promise); 205 | }; 206 | 207 | return polyfill('cancelAnimationFrame', fallback); 208 | } -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | var testFiles = [ 2 | 'bower_components/angular/angular.js', 3 | 'bower_components/angular-mocks/angular-mocks.js', 4 | 'src/**/module.js', 5 | 'src/**/*.js', 6 | 'test/unit/**/*spec.js' 7 | ]; 8 | 9 | module.exports = function (config) { 10 | config.set({ 11 | 12 | basePath : '../', 13 | 14 | files : testFiles, 15 | 16 | autoWatch : true, 17 | 18 | frameworks: ['jasmine'], 19 | 20 | browsers : ['Chrome'], 21 | 22 | plugins : [ 23 | 'karma-chrome-launcher', 24 | 'karma-jasmine' 25 | ], 26 | 27 | junitReporter : { 28 | outputFile: 'test_out/unit.xml', 29 | suite: 'unit' 30 | } 31 | 32 | }); 33 | }; 34 | 35 | module.exports.testFiles = testFiles; -------------------------------------------------------------------------------- /test/unit/panelsnap.spec.js: -------------------------------------------------------------------------------- 1 | describe('angular-panel-snap', function () { 2 | var $scope; 3 | 4 | beforeEach(module('akreitals.panel-snap')); 5 | 6 | beforeEach(inject(function ($rootScope) { 7 | $scope = $rootScope.$new(); 8 | })); 9 | 10 | /* 11 | * PanelGroupController 12 | */ 13 | describe('PanelGroupController', function () { 14 | var ctrl, $element, $attrs; 15 | beforeEach(inject(function ($controller) { 16 | $attrs = {}; 17 | $element = {}; 18 | ctrl = $controller('PanelGroupController', { $scope: $scope, $element: $element, $attrs: $attrs }); 19 | })); 20 | 21 | it('should add the specified panel to the panel group', function () { 22 | var panel1, panel2; 23 | ctrl.addPanel(group1 = $scope.$new()); 24 | ctrl.addPanel(group2 = $scope.$new()); 25 | expect(ctrl.panels.length).toBe(2); 26 | expect(ctrl.panels[0]).toBe(group1); 27 | expect(ctrl.panels[1]).toBe(group2); 28 | }); 29 | 30 | // it('should allow snapping to be enabled and disabled', function () { 31 | // expect(ctrl.enabled).toBe(true); 32 | // $scope.disableSnap(); 33 | // expect(ctrl.enabled).toBe(false); 34 | // $scope.enableSnap(); 35 | // expect(ctrl.enabled).toBe(true); 36 | // $scope.toggleSnap(); 37 | // expect(ctrl.enabled).toBe(false); 38 | // $scope.toggleSnap(); 39 | // expect(ctrl.enabled).toBe(true); 40 | // }); 41 | }); 42 | 43 | /* 44 | * akPanelGroup directive 45 | */ 46 | describe('panel-group', function () { 47 | var scope, $compile; 48 | var element, panels; 49 | 50 | beforeEach(inject(function (_$rootScope_, _$compile_, _$timeout_) { 51 | scope = _$rootScope_; 52 | $compile = _$compile_; 53 | $timeout = _$timeout_; 54 | 55 | var tpl = '' + 56 | 'First Panel' + 57 | 'Second Panel' + 58 | 'Third Panel' + 59 | ''; 60 | element = angular.element(tpl); 61 | element = $compile(element)(scope); 62 | scope.$digest(); 63 | panels = element.children(); 64 | })); 65 | 66 | afterEach(function () { 67 | element = panels = scope = $compile = undefined; 68 | }); 69 | 70 | it('should create panels with transcluded content', function () { 71 | expect(panels.length).toBe(3); 72 | expect(panels.eq(0).text()).toEqual('First Panel'); 73 | expect(panels.eq(1).text()).toEqual('Second Panel'); 74 | expect(panels.eq(2).text()).toEqual('Third Panel'); 75 | }); 76 | 77 | it('should mark first panel as active', function () { 78 | expect(panels.eq(0).hasClass('active')).toBe(true); 79 | expect(panels.eq(1).hasClass('active')).toBe(false); 80 | expect(panels.eq(2).hasClass('active')).toBe(false); 81 | }); 82 | 83 | /* TODO: test scrolling, cannot get scrollTop to work with karma, also 84 | elements all have 0 height when using tests for some reason so 85 | cannot be tested */ 86 | // it('should scroll to the next element', function () { 87 | // element.scrollTop(100); 88 | // element.trigger('scroll'); 89 | // $timeout.flush(); 90 | // expect(panels.eq(0).hasClass('active')).toBe(false); 91 | // expect(panels.eq(1).hasClass('active')).toBe(true); 92 | // }); 93 | }); 94 | }); --------------------------------------------------------------------------------