UNPKG

9.56 kBtext/coffeescriptView Raw
1# jQuery.fillwidth
2#
3# A plugin that given a `ul` with images inside their `lis` will do some things to line them up so
4# that everything fits inside their container nice and flush to the edges while retaining the
5# integrity of the original images (no cropping or skewing).
6#
7# Markup should be something like:
8# <ul>
9# <li>
10# <img>
11#
12$ = jQuery
13
14# Plugin globals
15totalPlugins = 0
16callQueue = []
17
18# In memory row and li objects
19# ----------------------------
20class Li
21
22 constructor: (el, settings) ->
23 $el = $(el)
24 @originalWidth = @width = $el.outerWidth(false)
25 @originalHeight = @height = $el.height()
26 @originalMargin = @margin = $el.outerWidth(true) - $el.outerWidth(false)
27 $img = $el.find('img')
28 @imgRatio = $img.width() / $img.height()
29 @$el = $el
30 @settings = settings
31
32 setHeight: (h) ->
33 @width = Math.round(h * (@width / @height))
34 @height = h unless @settings.lockedHeight # comment if locked height
35
36 setWidth: (w) ->
37 @height = Math.round(w * (@height / @width))
38 @width = w
39
40 decWidth: -> @setWidth @width - 1
41
42 decHeight: -> @setHeight @height - 1
43
44 incWidth: -> @setWidth @width + 1
45
46 incHeight: -> @setHeight @height + 1
47
48 updateDOM: ->
49 @$el.css
50 width : @width
51 height : @height
52 'margin-right' : @margin
53 @$el.find('img').height 'auto'
54
55 reset: ->
56 @width = @originalWidth
57 @height = @originalHeight
58 @margin = @originalMargin
59 @$el.css
60 width : @width
61 height : @height
62 'margin-right' : @margin
63
64class Row
65
66 constructor: (@frameWidth, @settings) ->
67 @lis = []
68
69 width: ->
70 width = 0
71 width += (li.width + li.margin) for li in @lis
72 width
73
74 updateDOM: ->
75 li.updateDOM() for li in @lis
76
77 # Resets the styling of the lis to be able to run calculations on a clean slate
78 reset: -> li.reset() for li in @lis
79
80 # Get an array of groups of landscapes in order of @settings.landscapeRatios
81 # e.g. [[li,li],[li,li,li]]
82 landscapeGroups: ->
83 landscapeGroups = []
84 for i, ratio of @settings.landscapeRatios
85 landscapes = (li for li in @lis when li.imgRatio >= ratio)
86 landscapeGroups.push landscapes
87 landscapeGroups
88
89 # Resize the landscape's height so that it fits the frame
90 resizeLandscapes: ->
91 for landscapes in @landscapeGroups(@settings.landscapeRatios) when landscapes.length isnt 0
92
93 # Reduce the landscapes until we are within the frame or beyond our threshold
94 for i in [0..@settings.resizeLandscapesBy]
95 li.decHeight() for li in landscapes
96 break if @width() <= @frameWidth
97 break if @width() <= @frameWidth
98 @
99
100 # Adjust the margins between list items to try an reach the frame
101 adjustMargins: ->
102 for i in [0..@settings.adjustMarginsBy]
103 for li in @lis[0..@lis.length - 2]
104 li.margin--
105 break if @width() <= @frameWidth
106 break if @width() <= @frameWidth
107
108 # Resize the entire row height by a maximum ammount in an attempt make the margins
109 resizeHeight: ->
110 i = 0
111 while @width() > @frameWidth and i < @settings.resizeRowBy
112 i++
113 li.decHeight() for li in @lis
114
115 # Round off all of the li's width
116 roundOff: ->
117 li.setWidth(Math.floor li.width) for li in @lis
118
119 # Arbitrarily extend lis to fill in any pixels that got rounded off
120 fillLeftoverPixels: ->
121 @roundOff()
122 diff = => @frameWidth - @width()
123
124 while diff() isnt 0
125 randIndex = Math.round Math.random() * (@lis.length - 1)
126 if diff() < 0
127 @lis[randIndex].decWidth()
128 else
129 @lis[randIndex].incWidth()
130
131 # Removes the right margin from the last row element
132 removeMargin: ->
133 lastLi = @lis[@lis.length - 1]
134 lastLi.margin = 0
135
136 # Make sure all of the lis are the same height (the tallest li in the group)
137 lockHeight: ->
138 tallestLi = (@lis.sort (a, b) -> b.height - a.height)[0]
139 tallestHeight = Math.ceil tallestLi.height
140 li.height = tallestHeight for li in @lis
141
142 # Go through the lis and hide them
143 hide: -> li.$el.hide() for li in @lis
144
145 # Go through the lis and show them
146 show: -> li.$el.show() for li in @lis
147
148# Debounce stolen from underscore.js
149# ----------------------------------
150debounce = (func, wait) ->
151 timeout = 0
152 return ->
153 args = arguments
154 throttler = =>
155 timeout = null
156 func args
157
158 clearTimeout timeout
159 timeout = setTimeout(throttler, wait)
160
161# Methods
162# -------
163methods =
164
165 # Called on initialization of the plugin
166 init: (settings) ->
167
168 # Settings
169 _defaults =
170 resizeLandscapesBy: 200
171 resizeRowBy: 30
172 landscapeRatios: (i / 10 for i in [10..50] by 3).reverse()
173 fillLastRow: false
174 beforeFillWidth: null
175 afterFillWidth: null
176 @settings = $.extend _defaults, settings
177
178 @each (i, el) =>
179 $el = $(el)
180 methods.initStyling.call @, $el
181
182 # Decide to run fillWidth after all of the child images have loaded, or before hand depending
183 # on whether the @settings to do the latter have been specified.
184 initFillWidth = =>
185 methods.fillWidth.call @, $el
186 # work around for iOS and IE8 continuous resize bug
187 # Cause: in iOS changing document height triggers a resize event
188 unless navigator.userAgent.match(/iPhone/i) or
189 navigator.userAgent.match(/iPad/i) or
190 navigator.userAgent.match(/iPod/i) or
191 navigator.userAgent.match(/MSIE 8\.0/i)
192 $(window).bind 'resize.fillwidth', debounce (=>
193 callQueue.push (=> methods.fillWidth.call @, $el)
194 if callQueue.length is totalPlugins
195 fn() for fn in callQueue
196 callQueue = []
197 ), 300
198 totalPlugins++
199
200 $imgs = $el.find('img')
201
202 if @settings.imageDimensions?
203 initFillWidth()
204 else
205 imagesToLoad = $imgs.length
206 $imgs.load ->
207 imagesToLoad--
208 initFillWidth() if imagesToLoad is 0
209
210 # Initial styling applied to the element to get lis to line up horizontally and images to be
211 # contained well in them.
212 initStyling: (el) ->
213 $el = $ el
214 $el.css
215 'list-style': 'none'
216 padding: 0
217 margin: 0
218 $el.css @settings.initStyling if @settings.initStyling?
219 $el.find('> li').css
220 'float': 'left'
221 'margin-left': 0
222 $el.find('img').css
223 'max-width': '100%'
224 'max-height': '100%'
225
226 # Set the initial width and height of the lis if passed in
227 if @settings and @settings.imageDimensions?
228 $el.find('> li').each (i, el) =>
229 $img = $(el).find('img').first()
230 $img.width @settings.imageDimensions[i].width
231 $img.height @settings.imageDimensions[i].height
232
233 # Removes the fillwidth functionality completely. Returns the element back to it's state
234 destroy: ->
235 $(window).unbind 'resize.fillwidth'
236 @each ->
237 row.reset() for row in $(@).fillwidth('rowObjs')
238 $(@).removeData('fillwidth.rows')
239
240 # Combines all of the magic and lines the lis up
241 fillWidth: (el) ->
242 $el = $ el
243 $el.trigger 'fillwidth.beforeFillWidth'
244 @settings.beforeFillWidth() if @settings.beforeFillWidth?
245
246 # Reset the list items & unfreeze the container
247 if @fillwidthRows
248 row.reset() for row in @fillwidthRows #$el.data 'fillwidth.rows'
249 $el.width 'auto'
250
251 $el.trigger 'fillwidth.beforeNewRows'
252 @settings.beforeNewRows() if @settings.beforeNewRows?
253
254 # Store the new row in-memory objects and re-freeze the container
255 @frameWidth = $el.width()
256 rows = methods.breakUpIntoRows.call @, $el
257 @fillwidthRows = rows
258 $el.width @frameWidth
259
260 $el.trigger 'fillwidth.afterNewRows'
261 @settings.afterNewRows() if @settings.afterNewRows?
262
263 # Go through each row and try various things to line up
264 for row in rows
265 continue unless row.lis.length > 1
266 row.removeMargin()
267 row.resizeHeight()
268 row.adjustMargins() if @settings.adjustMarginsBy?
269 row.resizeLandscapes()
270 row.fillLeftoverPixels() unless row is rows[rows.length - 1] and not @settings.fillLastRow
271 row.lockHeight()
272 row.updateDOM()
273
274 $el.trigger 'fillwidth.afterFillWidth'
275 @settings.afterFillWidth() if @settings.afterFillWidth?
276
277 # Returns the current in-memory row objects
278 rowObjs: ->
279 arr = []
280 rows = @fillwidthRows
281 @each ->
282 arr.push rows
283 arr = arr[0] if arr.length is 1
284 arr
285
286 # Returns an array of groups of li elements that make up a row
287 rows: ->
288 rows = methods.rowObjs.call @
289 arr = []
290 for row in rows
291 arr.push (li.$el for li in row.lis)
292 arr = arr[0] if arr.length is 1
293 arr
294
295 # Determine which set of lis go over the edge of the container, and store their
296 # { width, height, el, etc.. } in an array. Storing the width and height in objects helps run
297 # calculations without waiting for render reflows.
298 breakUpIntoRows: (el) ->
299 $el = $ el
300 i = 0
301 rows = [new Row(@frameWidth, @settings)]
302 $el.find('> li').each (j, li) =>
303 return if $(li).is(':hidden')
304 rows[i].lis.push new Li li, @settings
305 if rows[i].width() >= $el.width() and j isnt $el.find('> li').length - 1
306 rows.push new Row(@frameWidth, @settings)
307 i++
308 rows
309
310# Either call a method if passed a string, or call init if passed an object
311$.fn.fillwidth = (method) ->
312 if methods[method]?
313 methods[method].apply @, Array::slice.call(arguments, 1)
314 else if typeof method is "object" or not method?
315 methods.init.apply @, arguments
316 else
317 $.error "Method #{method} does not exist on jQuery.fillwidth"