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