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