├── .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&&aA 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 | 19 |Simply add an ak-panel-group-menu
directive to add a dynamic navigation menu.
You can disable the scroll, the menu will still be functional
34 | 35 | 36 | 37 |Use the left and right arrow keys to navigate this panel group (keys are customisable).
47 |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 |
First panel content
32 |Second panel content
37 |Third panel content
42 |First panel content
66 |Second panel content
71 | 72 |First panel content
93 |Second panel content
98 |Third panel content
103 |