1 /**
2 * Gumby Fixed
3 */
4 !function($) {
5 
6 	'use strict';
7 
8 	function Fixed($el) {
9 
10 		Gumby.debug('Initializing Fixed Position', $el);
11 
12 		this.$el = $el;
13 
14 		this.$window = $(window);
15 		this.fixedPoint = '';
16 		this.pinPoint = false;
17 		this.fixedPointjQ = false;
18 		this.pinPointjQ = false;
19 		this.offset = 0;
20 		this.pinOffset = 0;
21 		this.top = 0;
22 		this.constrainEl = true;
23 		this.state = false;
24 		this.measurements = {
25 			left: 0,
26 			width: 0
27 		};
28 
29 		// set up module based on attributes
30 		this.setup();
31 
32 		var scope = this;
33 
34 		// monitor scroll and update fixed elements accordingly
35 		this.$window.on('scroll load', function() {
36 			scope.monitorScroll();
37 		});
38 
39 		// reinitialize event listener
40 		this.$el.on('gumby.initialize', function() {
41 			Gumby.debug('Re-initializing Fixed Position', $el);
42 			scope.setup();
43 			scope.monitorScroll();
44 		});
45 	}
46 
47 	// set up module based on attributes
48 	Fixed.prototype.setup = function() {
49 		var scope = this;
50 
51 		this.fixedPoint = this.parseAttrValue(Gumby.selectAttr.apply(this.$el, ['fixed']));
52 
53 		// pin point is optional
54 		this.pinPoint = Gumby.selectAttr.apply(this.$el, ['pin']) || false;
55 
56 		// offset from fixed point
57 		this.offset = Number(Gumby.selectAttr.apply(this.$el, ['offset'])) || 0;
58 
59 		// offset from pin point
60 		this.pinOffset = Number(Gumby.selectAttr.apply(this.$el, ['pinoffset'])) || 0;
61 
62 		// top position when fixed
63 		this.top = Number(Gumby.selectAttr.apply(this.$el, ['top'])) || 0;
64 
65 		// constrain can be turned off
66 		this.constrainEl = Gumby.selectAttr.apply(this.$el, ['constrain']) || true;
67 		if(this.constrainEl === 'false') {
68 			this.constrainEl = false;
69 		}
70 
71 		// reference to the parent, row/column
72 		this.$parent = this.$el.parents('.columns, .column, .row');
73 		this.$parent = this.$parent.length ? this.$parent.first() : false;
74 		this.parentRow = this.$parent ? !!this.$parent.hasClass('row') : false;
75 
76 		// if optional pin point set then parse now
77 		if(this.pinPoint) {
78 			this.pinPoint = this.parseAttrValue(this.pinPoint);
79 		}
80 
81 		this.fixedPointjQ = this.fixedPoint instanceof jQuery;
82 		this.pinPointjQ = this.pinPoint instanceof jQuery;
83 
84 		// if we have a parent constrain dimenions
85 		if(this.$parent && this.constrainEl) {
86 			// measure up
87 			this.measure();
88 			// and on resize reset measurement
89 			this.$window.resize(function() {
90 				if(scope.state) {
91 					scope.measure();
92 					scope.constrain();
93 				}
94 			});
95 		}
96 	};
97 
98 	// monitor scroll and trigger changes based on position
99 	Fixed.prototype.monitorScroll = function() {
100 		var scrollAmount = this.$window.scrollTop(),
101 			// recalculate selector attributes as position may have changed
102 			fixedPoint = this.fixedPointjQ ? this.fixedPoint.offset().top : this.fixedPoint,
103 			pinPoint = false,
104 			timer;
105 
106 		// if a pin point is set recalculate
107 		if(this.pinPoint) {
108 			pinPoint = this.pinPointjQ ? this.pinPoint.offset().top : this.pinPoint;
109 		}
110 
111 		// apply offsets
112 		if(this.offset) { fixedPoint -= this.offset; }
113 		if(this.pinOffset) { pinPoint -= this.pinOffset; }
114 
115 		// fix it
116 		if((scrollAmount >= fixedPoint) && this.state !== 'fixed') {
117 			if(!pinPoint || scrollAmount < pinPoint) {
118 				this.fix();
119 			}
120 		// unfix it
121 		} else if(scrollAmount < fixedPoint && this.state === 'fixed') {
122 			this.unfix();
123 
124 		// pin it
125 		} else if(pinPoint && scrollAmount >= pinPoint && this.state !== 'pinned') {
126 			this.pin();
127 		}
128 	};
129 
130 	// fix the element and update state
131 	Fixed.prototype.fix = function() {
132 		Gumby.debug('Element has been fixed', this.$el);
133 		Gumby.debug('Triggering onFixed event', this.$el);
134 
135 		this.state = 'fixed';
136 		this.$el.css({
137 			'top' : this.top
138 		}).addClass('fixed').removeClass('unfixed pinned').trigger('gumby.onFixed');
139 
140 		// if we have a parent constrain dimenions
141 		if(this.$parent) {
142 			this.constrain();
143 		}
144 	};
145 
146 	// unfix the element and update state
147 	Fixed.prototype.unfix = function() {
148 		Gumby.debug('Element has been unfixed', this.$el);
149 		Gumby.debug('Triggering onUnfixed event', this.$el);
150 
151 		this.state = 'unfixed';
152 		this.$el.addClass('unfixed').removeClass('fixed pinned').trigger('gumby.onUnfixed');
153 	};
154 
155 	// pin the element in position
156 	Fixed.prototype.pin = function() {
157 		Gumby.debug('Element has been pinned', this.$el);
158 		Gumby.debug('Triggering onPinned event', this.$el);
159 		this.state = 'pinned';
160 		this.$el.css({
161 			'top' : this.$el.offset().top
162 		}).addClass('pinned fixed').removeClass('unfixed').trigger('gumby.onPinned');
163 	};
164 
165 	// constrain elements dimensions to match width/height
166 	Fixed.prototype.constrain = function() {
167 		Gumby.debug("Constraining element", this.$el);
168 		this.$el.css({
169 			left: this.measurements.left,
170 			width: this.measurements.width
171 		});
172 	};
173 
174 	// measure up the parent for constraining
175 	Fixed.prototype.measure = function() {
176 		var parentPadding;
177 
178 		this.measurements.left = this.$parent.offset().left;
179 		this.measurements.width = this.$parent.width();
180 
181 		// if element has a parent row then need to consider padding
182 		if(this.parentRow) {
183 			parentPadding = Number(this.$parent.css('paddingLeft').replace(/px/, ''));
184 			if(parentPadding) {
185 				this.measurements.left += parentPadding;
186 			}
187 		}
188 	};
189 
190 	// parse attribute values, could be px, top, selector
191 	Fixed.prototype.parseAttrValue = function(attr) {
192 		// px value fixed point
193 		if($.isNumeric(attr)) {
194 			return Number(attr);
195 		// 'top' string fixed point
196 		} else if(attr === 'top') {
197 			return this.$el.offset().top;
198 		// selector specified
199 		} else {
200 			var $el = $(attr);
201 			if(!$el.length) {
202 				Gumby.error('Cannot find Fixed target: '+attr);
203 				return false;
204 			}
205 			return $el;
206 		}
207 	};
208 
209 	// add initialisation
210 	Gumby.addInitalisation('fixed', function(all) {
211 		$('[data-fixed],[gumby-fixed],[fixed]').each(function() {
212 			var $this = $(this);
213 
214 			// this element has already been initialized
215 			// and we're only initializing new modules
216 			if($this.data('isFixed') && !all) {
217 				return true;
218 
219 			// this element has already been initialized
220 			// and we need to reinitialize it
221 			} else if($this.data('isFixed') && all) {
222 				$this.trigger('gumby.initialize');
223 				return true;
224 			}
225 
226 			// mark element as initialized
227 			$this.data('isFixed', true);
228 			new Fixed($this);
229 		});
230 	});
231 
232 	// register UI module
233 	Gumby.UIModule({
234 		module: 'fixed',
235 		events: ['initialize', 'onFixed', 'onUnfixed'],
236 		init: function() {
237 			Gumby.initialize('fixed');
238 		}
239 	});
240 }(jQuery);
241