commit | author | age
|
1759c2
|
1 |
/** |
RM |
2 |
* angular-strap |
|
3 |
* @version v2.0.3 - 2014-05-30 |
|
4 |
* @link http://mgcrea.github.io/angular-strap |
|
5 |
* @author Olivier Louvignes (olivier@mg-crea.com) |
|
6 |
* @license MIT License, http://www.opensource.org/licenses/MIT |
|
7 |
*/ |
|
8 |
'use strict'; |
|
9 |
angular.module('mgcrea.ngStrap.scrollspy', [ |
|
10 |
'mgcrea.ngStrap.helpers.debounce', |
|
11 |
'mgcrea.ngStrap.helpers.dimensions' |
|
12 |
]).provider('$scrollspy', function () { |
|
13 |
// Pool of registered spies |
|
14 |
var spies = this.$$spies = {}; |
|
15 |
var defaults = this.defaults = { |
|
16 |
debounce: 150, |
|
17 |
throttle: 100, |
|
18 |
offset: 100 |
|
19 |
}; |
|
20 |
this.$get = [ |
|
21 |
'$window', |
|
22 |
'$document', |
|
23 |
'$rootScope', |
|
24 |
'dimensions', |
|
25 |
'debounce', |
|
26 |
'throttle', |
|
27 |
function ($window, $document, $rootScope, dimensions, debounce, throttle) { |
|
28 |
var windowEl = angular.element($window); |
|
29 |
var docEl = angular.element($document.prop('documentElement')); |
|
30 |
var bodyEl = angular.element($window.document.body); |
|
31 |
// Helper functions |
|
32 |
function nodeName(element, name) { |
|
33 |
return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase(); |
|
34 |
} |
|
35 |
function ScrollSpyFactory(config) { |
|
36 |
// Common vars |
|
37 |
var options = angular.extend({}, defaults, config); |
|
38 |
if (!options.element) |
|
39 |
options.element = bodyEl; |
|
40 |
var isWindowSpy = nodeName(options.element, 'body'); |
|
41 |
var scrollEl = isWindowSpy ? windowEl : options.element; |
|
42 |
var scrollId = isWindowSpy ? 'window' : options.id; |
|
43 |
// Use existing spy |
|
44 |
if (spies[scrollId]) { |
|
45 |
spies[scrollId].$$count++; |
|
46 |
return spies[scrollId]; |
|
47 |
} |
|
48 |
var $scrollspy = {}; |
|
49 |
// Private vars |
|
50 |
var unbindViewContentLoaded, unbindIncludeContentLoaded; |
|
51 |
var trackedElements = $scrollspy.$trackedElements = []; |
|
52 |
var sortedElements = []; |
|
53 |
var activeTarget; |
|
54 |
var debouncedCheckPosition; |
|
55 |
var throttledCheckPosition; |
|
56 |
var debouncedCheckOffsets; |
|
57 |
var viewportHeight; |
|
58 |
var scrollTop; |
|
59 |
$scrollspy.init = function () { |
|
60 |
// Setup internal ref counter |
|
61 |
this.$$count = 1; |
|
62 |
// Bind events |
|
63 |
debouncedCheckPosition = debounce(this.checkPosition, options.debounce); |
|
64 |
throttledCheckPosition = throttle(this.checkPosition, options.throttle); |
|
65 |
scrollEl.on('click', this.checkPositionWithEventLoop); |
|
66 |
windowEl.on('resize', debouncedCheckPosition); |
|
67 |
scrollEl.on('scroll', throttledCheckPosition); |
|
68 |
debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce); |
|
69 |
unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets); |
|
70 |
unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets); |
|
71 |
debouncedCheckOffsets(); |
|
72 |
// Register spy for reuse |
|
73 |
if (scrollId) { |
|
74 |
spies[scrollId] = $scrollspy; |
|
75 |
} |
|
76 |
}; |
|
77 |
$scrollspy.destroy = function () { |
|
78 |
// Check internal ref counter |
|
79 |
this.$$count--; |
|
80 |
if (this.$$count > 0) { |
|
81 |
return; |
|
82 |
} |
|
83 |
// Unbind events |
|
84 |
scrollEl.off('click', this.checkPositionWithEventLoop); |
|
85 |
windowEl.off('resize', debouncedCheckPosition); |
|
86 |
scrollEl.off('scroll', debouncedCheckPosition); |
|
87 |
unbindViewContentLoaded(); |
|
88 |
unbindIncludeContentLoaded(); |
|
89 |
if (scrollId) { |
|
90 |
delete spies[scrollId]; |
|
91 |
} |
|
92 |
}; |
|
93 |
$scrollspy.checkPosition = function () { |
|
94 |
// Not ready yet |
|
95 |
if (!sortedElements.length) |
|
96 |
return; |
|
97 |
// Calculate the scroll position |
|
98 |
scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0; |
|
99 |
// Calculate the viewport height for use by the components |
|
100 |
viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight')); |
|
101 |
// Activate first element if scroll is smaller |
|
102 |
if (scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) { |
|
103 |
return $scrollspy.$activateElement(sortedElements[0]); |
|
104 |
} |
|
105 |
// Activate proper element |
|
106 |
for (var i = sortedElements.length; i--;) { |
|
107 |
if (angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) |
|
108 |
continue; |
|
109 |
if (activeTarget === sortedElements[i].target) |
|
110 |
continue; |
|
111 |
if (scrollTop < sortedElements[i].offsetTop) |
|
112 |
continue; |
|
113 |
if (sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) |
|
114 |
continue; |
|
115 |
return $scrollspy.$activateElement(sortedElements[i]); |
|
116 |
} |
|
117 |
}; |
|
118 |
$scrollspy.checkPositionWithEventLoop = function () { |
|
119 |
setTimeout(this.checkPosition, 1); |
|
120 |
}; |
|
121 |
// Protected methods |
|
122 |
$scrollspy.$activateElement = function (element) { |
|
123 |
if (activeTarget) { |
|
124 |
var activeElement = $scrollspy.$getTrackedElement(activeTarget); |
|
125 |
if (activeElement) { |
|
126 |
activeElement.source.removeClass('active'); |
|
127 |
if (nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) { |
|
128 |
activeElement.source.parent().parent().removeClass('active'); |
|
129 |
} |
|
130 |
} |
|
131 |
} |
|
132 |
activeTarget = element.target; |
|
133 |
element.source.addClass('active'); |
|
134 |
if (nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) { |
|
135 |
element.source.parent().parent().addClass('active'); |
|
136 |
} |
|
137 |
}; |
|
138 |
$scrollspy.$getTrackedElement = function (target) { |
|
139 |
return trackedElements.filter(function (obj) { |
|
140 |
return obj.target === target; |
|
141 |
})[0]; |
|
142 |
}; |
|
143 |
// Track offsets behavior |
|
144 |
$scrollspy.checkOffsets = function () { |
|
145 |
angular.forEach(trackedElements, function (trackedElement) { |
|
146 |
var targetElement = document.querySelector(trackedElement.target); |
|
147 |
trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null; |
|
148 |
if (options.offset && trackedElement.offsetTop !== null) |
|
149 |
trackedElement.offsetTop -= options.offset * 1; |
|
150 |
}); |
|
151 |
sortedElements = trackedElements.filter(function (el) { |
|
152 |
return el.offsetTop !== null; |
|
153 |
}).sort(function (a, b) { |
|
154 |
return a.offsetTop - b.offsetTop; |
|
155 |
}); |
|
156 |
debouncedCheckPosition(); |
|
157 |
}; |
|
158 |
$scrollspy.trackElement = function (target, source) { |
|
159 |
trackedElements.push({ |
|
160 |
target: target, |
|
161 |
source: source |
|
162 |
}); |
|
163 |
}; |
|
164 |
$scrollspy.untrackElement = function (target, source) { |
|
165 |
var toDelete; |
|
166 |
for (var i = trackedElements.length; i--;) { |
|
167 |
if (trackedElements[i].target === target && trackedElements[i].source === source) { |
|
168 |
toDelete = i; |
|
169 |
break; |
|
170 |
} |
|
171 |
} |
|
172 |
trackedElements = trackedElements.splice(toDelete, 1); |
|
173 |
}; |
|
174 |
$scrollspy.activate = function (i) { |
|
175 |
trackedElements[i].addClass('active'); |
|
176 |
}; |
|
177 |
// Initialize plugin |
|
178 |
$scrollspy.init(); |
|
179 |
return $scrollspy; |
|
180 |
} |
|
181 |
return ScrollSpyFactory; |
|
182 |
} |
|
183 |
]; |
|
184 |
}).directive('bsScrollspy', [ |
|
185 |
'$rootScope', |
|
186 |
'debounce', |
|
187 |
'dimensions', |
|
188 |
'$scrollspy', |
|
189 |
function ($rootScope, debounce, dimensions, $scrollspy) { |
|
190 |
return { |
|
191 |
restrict: 'EAC', |
|
192 |
link: function postLink(scope, element, attr) { |
|
193 |
var options = { scope: scope }; |
|
194 |
angular.forEach([ |
|
195 |
'offset', |
|
196 |
'target' |
|
197 |
], function (key) { |
|
198 |
if (angular.isDefined(attr[key])) |
|
199 |
options[key] = attr[key]; |
|
200 |
}); |
|
201 |
var scrollspy = $scrollspy(options); |
|
202 |
scrollspy.trackElement(options.target, element); |
|
203 |
scope.$on('$destroy', function () { |
|
204 |
scrollspy.untrackElement(options.target, element); |
|
205 |
scrollspy.destroy(); |
|
206 |
options = null; |
|
207 |
scrollspy = null; |
|
208 |
}); |
|
209 |
} |
|
210 |
}; |
|
211 |
} |
|
212 |
]).directive('bsScrollspyList', [ |
|
213 |
'$rootScope', |
|
214 |
'debounce', |
|
215 |
'dimensions', |
|
216 |
'$scrollspy', |
|
217 |
function ($rootScope, debounce, dimensions, $scrollspy) { |
|
218 |
return { |
|
219 |
restrict: 'A', |
|
220 |
compile: function postLink(element, attr) { |
|
221 |
var children = element[0].querySelectorAll('li > a[href]'); |
|
222 |
angular.forEach(children, function (child) { |
|
223 |
var childEl = angular.element(child); |
|
224 |
childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href')); |
|
225 |
}); |
|
226 |
} |
|
227 |
}; |
|
228 |
} |
|
229 |
]); |