Skip to content

Commit 0ba5876

Browse files
committed
WIP: slidebox
1 parent 7d000de commit 0ba5876

File tree

15 files changed

+723
-304
lines changed

15 files changed

+723
-304
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
IonicModule
2+
.controller('$ionSlideBox', [
3+
'$scope',
4+
'$element',
5+
SlideBoxController
6+
]);
7+
8+
/*
9+
* This can be abstracted into a controller that will work for views, tabs, and slidebox.
10+
*/
11+
function SlideBoxController(scope, element) {
12+
var self = this;
13+
var slideList = [];
14+
var selectedIndex = -1;
15+
var previousIndex = -1;
16+
var dragElement = angular.element(element[0].querySelector('.slider-slides'));
17+
18+
// Successful slide requires velocity to be greater than this amount
19+
var SLIDE_SUCCESS_VELOCITY = (1 / 4); // pixels / ms
20+
21+
self.dragElement = dragElement;
22+
self.element = element;
23+
24+
self.setLooping = setLooping;
25+
26+
self.add = add;
27+
self.remove = remove;
28+
self.move = move;
29+
30+
self.shown = shown;
31+
self.at = at;
32+
self.isInRange = isInRange;
33+
self.indexOf = indexOf;
34+
self.count = count;
35+
self.left = left;
36+
self.right = right;
37+
self.delta = delta;
38+
self.isRelevant = isRelevant;
39+
40+
self.select = select;
41+
self.onDrag = onDrag;
42+
self.onDragEnd = onDragEnd;
43+
44+
SlideDragController(scope, element, self);
45+
46+
function setLooping(isLooping) {
47+
self.isLooping = !!isLooping;
48+
}
49+
50+
/*
51+
* Add/remove/move slides
52+
*/
53+
function add(slide, index) {
54+
if (arguments.length === 1) index = slideList.length;
55+
56+
slideList.splice(index, 0, slide);
57+
slide.onAdded(dragElement);
58+
59+
if (selectedIndex === -1) {
60+
self.select(index);
61+
62+
// if the new slide is adjacent to selected, refresh the selection
63+
} else if (index === self.left() || index === self.right()) {
64+
self.select(selectedIndex);
65+
}
66+
}
67+
function remove(slide) {
68+
var index = self.indexOf(slide);
69+
if (index < 0) return;
70+
71+
var isSelected = self.shown() === index;
72+
if (isSelected && self.length > 1) {
73+
self.select( self.isInRange(self.left()) ? self.left() : self.right() );
74+
}
75+
76+
slideList.splice(index, 1);
77+
slide.onRemoved();
78+
}
79+
function move(slide, toIndex) {
80+
var currentIndex = self.indexOf(slide);
81+
if (currentIndex < 0) return;
82+
83+
// If the slide is current, right, or left, save that
84+
// so we can re-select after moving.
85+
var isRelevant = self.isRelevant(toIndex);
86+
slideList.splice(currentIndex, 1);
87+
slideList.splice(toIndex, 0, slide);
88+
89+
if (isRelevant) {
90+
self.select(self.shown());
91+
}
92+
}
93+
94+
/*
95+
* Array helpers
96+
*/
97+
function shown() {
98+
return selectedIndex;
99+
}
100+
function at(index) {
101+
return slideList[index];
102+
}
103+
function isInRange(index) {
104+
return index >= 0 && index < slideList.length;
105+
}
106+
function indexOf(slide) {
107+
return slideList.indexOf(slide);
108+
}
109+
function count() {
110+
return slideList.length;
111+
}
112+
113+
// Get the right slide relative to the given slide
114+
function right(fromIndex) {
115+
if (arguments.length === 0) fromIndex = selectedIndex;
116+
if (!self.isInRange(fromIndex)) return -1;
117+
118+
var nextIndex = fromIndex + 1;
119+
if (self.isLooping && nextIndex === slideList.length) {
120+
nextIndex -= slideList.length;
121+
}
122+
return nextIndex;
123+
}
124+
125+
// Get the left slide relative to the given slide
126+
// Arbitrarily say we can't have a left with less than 3 slides.
127+
// This is because if only 2 slides exist, left === right.
128+
function left(fromIndex) {
129+
if (arguments.length === 0) fromIndex = selectedIndex;
130+
if (!self.isInRange(fromIndex) || slideList.length < 3) return -1;
131+
132+
var previousIndex = fromIndex - 1;
133+
if (self.isLooping && previousIndex === -1) {
134+
previousIndex += slideList.length;
135+
}
136+
return previousIndex;
137+
}
138+
139+
function delta(fromIndex, toIndex) {
140+
var difference = toIndex - fromIndex;
141+
if (!self.isLooping) return difference;
142+
143+
// Is looping on? Then check for the looped difference.
144+
// For example, going from the first slide to the last slide
145+
// is actually a change of -1.
146+
var loopedDifference = 0;
147+
if (toIndex > fromIndex) {
148+
loopedDifference = toIndex - fromIndex - self.count();
149+
} else {
150+
loopedDifference = self.count() - fromIndex + toIndex;
151+
}
152+
153+
if (Math.abs(loopedDifference) < Math.abs(difference)) {
154+
return loopedDifference;
155+
}
156+
return difference;
157+
}
158+
159+
// Returns true if the given index matches the shown, left, or right slide.
160+
function isRelevant(index) {
161+
return self.isInRange(index) && (
162+
index === self.shown() ||
163+
index === self.left() ||
164+
index === self.right()
165+
);
166+
}
167+
168+
169+
/*
170+
* Select and change slides
171+
*/
172+
function select(newIndex) {
173+
if (!self.isInRange(newIndex)) return;
174+
175+
var delta = self.delta(selectedIndex, newIndex);
176+
if (self.isInRange(selectedIndex)) {
177+
// if the new slide is > 1 away, then it is currently not attached to the DOM.
178+
// Attach it in the position from which it will slide in.
179+
if (delta > 1) {
180+
self.at(newIndex).setState('right');
181+
} else if (delta < -1) {
182+
self.at(newIndex).setState('left');
183+
}
184+
}
185+
186+
selectedIndex = newIndex;
187+
188+
var rafId = self._rafId = ionic.requestAnimationFrame(doSelect);
189+
function doSelect() {
190+
// If a new selection has happened before this frame, abort.
191+
if (self._rafId !== rafId) return;
192+
scope.$evalAsync(function() {
193+
// If a new selection has happened before this frame, abort.
194+
if (self._rafId !== rafId) return;
195+
arrangeSlides(self.left(newIndex), newIndex, self.right(newIndex));
196+
});
197+
}
198+
}
199+
200+
var oldSlides;
201+
function arrangeSlides(left, shown, right) {
202+
var newSlides = {
203+
left: self.at(left),
204+
shown: self.at(shown),
205+
right: self.at(right)
206+
};
207+
208+
newSlides.left && newSlides.left.setState('left');
209+
newSlides.shown && newSlides.shown.setState('shown');
210+
newSlides.right && newSlides.right.setState('right');
211+
212+
if (oldSlides) {
213+
214+
var oldShown = oldSlides.shown;
215+
var delta = self.delta(indexOf(oldSlides.shown), indexOf(newSlides.shown));
216+
if (Math.abs(delta) > 1) {
217+
// If we're changing by more than one slide, we need to manually transition
218+
// the current slide out and then put it into its new state.
219+
oldShown.setState(delta > 1 ? 'left' : 'right').then(function() {
220+
oldShown.setState(
221+
newSlides.left === oldShown ? 'left' :
222+
newSlides.right === oldShown ? 'right' :
223+
'detached'
224+
);
225+
});
226+
} else {
227+
detachIfUnused(oldSlides.shown);
228+
}
229+
//Additionally, we need to detach both of the old slides.
230+
detachIfUnused(oldSlides.left);
231+
detachIfUnused(oldSlides.right);
232+
}
233+
234+
function detachIfUnused(oldSlide) {
235+
if (oldSlide && oldSlide !== newSlides.left &&
236+
oldSlide !== newSlides.shown &&
237+
oldSlide !== newSlides.right) {
238+
oldSlide.setState('detached');
239+
}
240+
}
241+
242+
oldSlides = newSlides;
243+
}
244+
245+
function onDrag(percent) {
246+
var target = self.at(percent > 0 ? self.right() : self.left());
247+
var current = self.at(self.shown());
248+
249+
target && target.transform(percent);
250+
current && current.transform(percent);
251+
}
252+
253+
function onDragEnd(percent, velocity) {
254+
scope.$evalAsync(function() {
255+
var nextIndex;
256+
if (Math.abs(percent) > 0.5 || velocity > SLIDE_SUCCESS_VELOCITY) {
257+
nextIndex = percent > 0 ? self.right() : self.left();
258+
}
259+
self.select( self.isInRange(nextIndex) ? nextIndex : self.shown() );
260+
});
261+
}
262+
263+
}
264+
265+
function SlideDragController(scope, element, slideBox) {
266+
var dragElement = slideBox.dragElement;
267+
268+
var dragStartGesture = ionic.onGesture('dragstart', onDragStart, dragElement[0]);
269+
var dragGesture = ionic.onGesture('drag', onDrag, dragElement[0]);
270+
var dragEndGesture = ionic.onGesture('dragend', onDragEnd, dragElement[0]);
271+
scope.$on('$destroy', function() {
272+
ionic.offGesture(dragStartGesture);
273+
ionic.offGesture(dragGesture);
274+
ionic.offGesture(dragEndGesture);
275+
});
276+
277+
// Stop scroll from propagating up
278+
dragElement.on('touchmove mousemove pointermove', function(e) {
279+
if (dragState && dragState.dragging) e.preventDefault();
280+
});
281+
282+
var dragState;
283+
function onDragStart(ev) {
284+
if (dragState) return;
285+
dragState = {
286+
startX: ev.gesture.center.pageX,
287+
startY: ev.gesture.center.pageY,
288+
width: dragElement.prop('offsetWidth')
289+
};
290+
}
291+
function onDrag(ev) {
292+
if (!dragState) return;
293+
var deltaX = dragState.startX - ev.gesture.center.pageX;
294+
var deltaY = dragState.startY - ev.gesture.center.pageY;
295+
296+
if (ev.gesture.direction === 'up' || ev.gesture.direction === 'down') {
297+
if (Math.abs(deltaY) > Math.abs(deltaX)) {
298+
onDragEnd(ev);
299+
}
300+
return;
301+
}
302+
dragState.dragging = true;
303+
304+
var percent = getDragPercent(ev.gesture.center.pageX);
305+
slideBox.onDrag(percent);
306+
}
307+
308+
function onDragEnd(ev) {
309+
if (!dragState) return;
310+
var percent = getDragPercent(ev.gesture.center.pageX);
311+
slideBox.onDragEnd(percent, ev.gesture.velocityX);
312+
dragState = null;
313+
}
314+
315+
function getDragPercent(x) {
316+
var delta = dragState.startX - x;
317+
var percent = delta / dragState.width;
318+
return percent;
319+
}
320+
321+
}

0 commit comments

Comments
 (0)