alongslide 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +10 -0
- data/Gemfile.lock +103 -0
- data/alongslide.gemspec +18 -0
- data/app/assets/javascripts/alongslide/alongslide.coffee +107 -0
- data/app/assets/javascripts/alongslide/layout.coffee +504 -0
- data/app/assets/javascripts/alongslide/parser.coffee +125 -0
- data/app/assets/javascripts/alongslide/scrolling.coffee +423 -0
- data/app/assets/javascripts/alongslide.coffee +19 -0
- data/app/assets/stylesheets/alongslide.sass +444 -0
- data/app/views/panel/panel.haml +8 -0
- data/app/views/panel/unpin.haml +4 -0
- data/app/views/section/exit.haml +4 -0
- data/app/views/section/section.haml +4 -0
- data/grammar/alongslide.treetop +162 -0
- data/grammar/panel.treetop +67 -0
- data/vendor/assets/javascripts/jquery.fitvids.js +74 -0
- data/vendor/assets/javascripts/prefix.js +18 -0
- data/vendor/assets/javascripts/regionFlow.coffee +305 -0
- data/vendor/assets/javascripts/skrollr.js +1716 -0
- data/vendor/assets/javascripts/tether.js +1357 -0
- metadata +23 -3
@@ -0,0 +1,504 @@
|
|
1
|
+
#
|
2
|
+
# alongslide.coffee: Re-format HTML into horizontally-scrolling elements
|
3
|
+
# which scroll at different rates.
|
4
|
+
#
|
5
|
+
# Use CSS Regions polyfill for text flowing, and skrollr for scroll positioning.
|
6
|
+
#
|
7
|
+
# Copyright 2013 Canopy Canopy Canopy, Inc.
|
8
|
+
# Authors Adam Florin & Anthony Tran
|
9
|
+
#
|
10
|
+
class Alongslide::Layout
|
11
|
+
|
12
|
+
# For parsing pinned panel directives.
|
13
|
+
#
|
14
|
+
HORIZONTAL_EDGES: ["left", "right"]
|
15
|
+
VERTICAL_EDGES: ["top", "bottom"]
|
16
|
+
EDGES: @::HORIZONTAL_EDGES.concat @::VERTICAL_EDGES
|
17
|
+
SIZES: ["one-third", "half", "two-thirds"]
|
18
|
+
ALIGNMENTS: @::EDGES.concat 'fullscreen'
|
19
|
+
|
20
|
+
# Keys to ALS position data attributes
|
21
|
+
IN_POINT_KEY: 'als-in-position'
|
22
|
+
OUT_POINT_KEY: 'als-out-position'
|
23
|
+
|
24
|
+
# Pixel width of each frame == screen width.
|
25
|
+
#
|
26
|
+
frameWidth: $(window).width()
|
27
|
+
|
28
|
+
# Switch to true for verbose debugging. Plus constants for indent level.
|
29
|
+
#
|
30
|
+
debug: false
|
31
|
+
SUPER_FRAME_LEVEL: 1
|
32
|
+
FRAME_LEVEL: 2
|
33
|
+
SUB_FRAME_LEVEL: 3
|
34
|
+
|
35
|
+
#
|
36
|
+
#
|
37
|
+
constructor: (options = {}) ->
|
38
|
+
{@frames, @flowNames, @backgrounds, @panels, @regionCls, @sourceLength} = options
|
39
|
+
|
40
|
+
# Main entrypoint for asynchronous chain of render calls.
|
41
|
+
#
|
42
|
+
# @param postRenderCallback - to be called when layout is 100% complete
|
43
|
+
#
|
44
|
+
render: (@postRenderCallback) ->
|
45
|
+
@reset()
|
46
|
+
@writeBackgrounds()
|
47
|
+
@layout()
|
48
|
+
|
49
|
+
# Flow text into columns, using regionFlow as a CSS regions polyfill.
|
50
|
+
#
|
51
|
+
# Loop through each flow (= section), then each frame, then each column.
|
52
|
+
#
|
53
|
+
# This is all done asynchronously so that DOM can update itself periodically
|
54
|
+
# (namely for layout progress updates).
|
55
|
+
#
|
56
|
+
layout: =>
|
57
|
+
@startTime = new Date
|
58
|
+
@log "Beginning layout"
|
59
|
+
|
60
|
+
@currentFlowIndex = 0
|
61
|
+
|
62
|
+
# kick off asynchronous render
|
63
|
+
@renderSection()
|
64
|
+
|
65
|
+
# Render one section (a.k.a. one "flow" in CSSRegions parlance),
|
66
|
+
# one frame at a time, asynchronously.
|
67
|
+
#
|
68
|
+
renderSection: ->
|
69
|
+
flowName = @flowNames[@currentFlowIndex]
|
70
|
+
|
71
|
+
@log "Laying out section \"#{flowName}\"", @SUPER_FRAME_LEVEL
|
72
|
+
background = @findBackground(flowName)
|
73
|
+
@setPositionOf background, to: @nextFramePosition() if background.length
|
74
|
+
background.addClass('unstaged')
|
75
|
+
|
76
|
+
@renderFrame(flowName)
|
77
|
+
|
78
|
+
# Render one frame and its containing columns.
|
79
|
+
#
|
80
|
+
# When frame is done, trigger (asynchronously) either next frame or next section.
|
81
|
+
#
|
82
|
+
# Note that normally we check _second-to-last_ column for directives,
|
83
|
+
# as last column contains overflow. Once flow is complete, though,
|
84
|
+
# check the last column--and remove it if it contains nothing but directives.
|
85
|
+
#
|
86
|
+
renderFrame: (flowName, frame, lastColumn) ->
|
87
|
+
frame = @findOrBuildNextFlowFrame frame
|
88
|
+
|
89
|
+
# for each column in frame
|
90
|
+
while frame.find('.'+@regionCls).length < @numFrameColumns(frame)
|
91
|
+
column = @buildRegion frame, flowName
|
92
|
+
|
93
|
+
# Move three-columns class from .section to .frame
|
94
|
+
hasThreeColumns = (column.children('.three-columns').length)
|
95
|
+
if hasThreeColumns
|
96
|
+
column.find('.section').removeClass('three-columns')
|
97
|
+
column.parent().addClass('three-columns')
|
98
|
+
|
99
|
+
# Process N-1 column (as current column still total text overflow of
|
100
|
+
# the entire section).
|
101
|
+
if lastColumn?
|
102
|
+
@checkForOrphans lastColumn
|
103
|
+
@updateProgress lastColumn
|
104
|
+
@checkForDirectives lastColumn
|
105
|
+
|
106
|
+
# Section is complete. Current column is the last column of the
|
107
|
+
# section (no longer the overflow column).
|
108
|
+
if @flowComplete(flowName)
|
109
|
+
@updateProgress column
|
110
|
+
@checkForDirectives column, true
|
111
|
+
@checkForEmpties column
|
112
|
+
|
113
|
+
# render next section, or complete render.
|
114
|
+
@currentFlowIndex++
|
115
|
+
unless @currentFlowIndex is @flowNames.length
|
116
|
+
background = @findBackground(flowName)
|
117
|
+
@setPositionOf background, until: @lastFramePosition() if background.length
|
118
|
+
setTimeout((=> @renderSection()), 1)
|
119
|
+
else
|
120
|
+
@log "Layout complete"
|
121
|
+
@reorder()
|
122
|
+
@index()
|
123
|
+
@frames.children('.flow').find('.frame').addClass('unstaged')
|
124
|
+
@postRenderCallback(@lastFramePosition())
|
125
|
+
return
|
126
|
+
|
127
|
+
lastColumn = column
|
128
|
+
|
129
|
+
# unstage earlier frames
|
130
|
+
frame.prevAll().addClass('unstaged')
|
131
|
+
|
132
|
+
# render next frame
|
133
|
+
setTimeout((=> @renderFrame(flowName, frame, lastColumn)), 1)
|
134
|
+
|
135
|
+
# Check the last "fit" column for any special directives (CSS classes).
|
136
|
+
#
|
137
|
+
# The last "fit" column is the last one that doesn't also contain all the
|
138
|
+
# overflow to be laid out in other columns--typically the second-to-last column.
|
139
|
+
#
|
140
|
+
# Then, having found a directive, parse the classes and act accordingly.
|
141
|
+
#
|
142
|
+
# NOTE that this method also takes responsibility for creating the appropriate
|
143
|
+
# next frame for text to flow in. It doesn't need to return it, however,
|
144
|
+
# as the caller will just use `findOrBuildNextFlowFrame` to check for it.
|
145
|
+
#
|
146
|
+
# @param column - jQuery element to scan for directives
|
147
|
+
# @param layoutComplete - true if this flow has been completely laid out
|
148
|
+
# (and therefore no new flowing regions should be created)
|
149
|
+
#
|
150
|
+
checkForDirectives: (column, layoutComplete) ->
|
151
|
+
# for each directive
|
152
|
+
(column.find ".alongslide").each (index, directiveElement) =>
|
153
|
+
directive = $(directiveElement).detach()
|
154
|
+
id = directive.data('alongslide-id')
|
155
|
+
|
156
|
+
# the column's frame may have changed (if specified by previous directive)
|
157
|
+
flowFrame = column.parent('.frame')
|
158
|
+
|
159
|
+
switch
|
160
|
+
# new panel
|
161
|
+
when directive.hasClass "show"
|
162
|
+
|
163
|
+
# build next flow frame (if there is one)
|
164
|
+
unless layoutComplete
|
165
|
+
nextFlowFrame = @findOrBuildNextFlowFrame flowFrame
|
166
|
+
nextFlowFramePosition = if nextFlowFrame
|
167
|
+
@getPositionOf(nextFlowFrame)
|
168
|
+
else
|
169
|
+
@nextFramePosition()
|
170
|
+
|
171
|
+
# build panel
|
172
|
+
panelPosition = if directive.hasClass("now")
|
173
|
+
@getPositionOf(flowFrame)
|
174
|
+
else
|
175
|
+
nextFlowFramePosition
|
176
|
+
panelFrame = @buildPanel id, panelPosition
|
177
|
+
|
178
|
+
@updateProgress(panelFrame)
|
179
|
+
|
180
|
+
switch
|
181
|
+
|
182
|
+
# pinned panel layout
|
183
|
+
when directive.hasClass "pin"
|
184
|
+
# display forever (until unpinned)
|
185
|
+
@setPositionOf panelFrame, until: -1
|
186
|
+
|
187
|
+
# which frames need to have classes set--next and/or current?
|
188
|
+
framesWithPinnedPanels = _.compact [
|
189
|
+
flowFrame if directive.hasClass("now"),
|
190
|
+
nextFlowFrame]
|
191
|
+
|
192
|
+
# set frame classes--pushing columns into subsequent frames if necessary
|
193
|
+
_.each framesWithPinnedPanels, (frame) =>
|
194
|
+
@log "Applying with-pinned-panel styles to flow frame at " +
|
195
|
+
"#{@getPositionOf frame}", @SUB_FRAME_LEVEL
|
196
|
+
frame.addClass @withPinnedClass(directive)
|
197
|
+
frame.addClass @withSizedClass(directive)
|
198
|
+
while frame.find('.'+@regionCls).length > @numFrameColumns frame
|
199
|
+
pushToFrame = @findOrBuildNextFlowFrame frame
|
200
|
+
@log "Pushing last column of flow frame at #{@getPositionOf frame} " +
|
201
|
+
"to flow frame at #{@getPositionOf pushToFrame}", @SUB_FRAME_LEVEL
|
202
|
+
frame.find('.'+@regionCls).last().detach().prependTo(pushToFrame)
|
203
|
+
|
204
|
+
# If we changed this frame's layout, re-flow this whole section's
|
205
|
+
# regions.
|
206
|
+
if directive.hasClass("now")
|
207
|
+
sectionId = flowFrame.data('als-section-id')
|
208
|
+
@frames.children('.flow').find(".frame[data-als-section-id=#{sectionId}]").removeClass('unstaged')
|
209
|
+
document.namedFlows.get(sectionId).reFlow()
|
210
|
+
|
211
|
+
# fullscreen panel layout
|
212
|
+
when directive.hasClass "fullscreen"
|
213
|
+
if directive.hasClass "now"
|
214
|
+
@setPositionOf flowFrame, to: nextFlowFramePosition
|
215
|
+
|
216
|
+
if nextFlowFrame?
|
217
|
+
@setPositionOf nextFlowFrame, to: nextFlowFramePosition + 1
|
218
|
+
|
219
|
+
# unpin pinned panel
|
220
|
+
when directive.hasClass "unpin"
|
221
|
+
panelFrame = @findPanel(id)
|
222
|
+
|
223
|
+
unless panelFrame.length == 0
|
224
|
+
@setPositionOf panelFrame, until:
|
225
|
+
if layoutComplete
|
226
|
+
@nextFramePosition() - 1
|
227
|
+
else
|
228
|
+
Math.max @getPositionOf(flowFrame), @getPositionOf(panelFrame)
|
229
|
+
|
230
|
+
# unset frame classes for first flow frame after panel (may be next-next frame)
|
231
|
+
unless layoutComplete
|
232
|
+
postPanelFlowFrame = @findOrBuildNextFlowFrame flowFrame
|
233
|
+
if @getPositionOf(postPanelFlowFrame) is @getPositionOf(panelFrame)
|
234
|
+
postPanelFlowFrame = @findOrBuildNextFlowFrame postPanelFlowFrame
|
235
|
+
postPanelFlowFrame.removeClass @withPinnedClass(panelFrame)
|
236
|
+
postPanelFlowFrame.removeClass @withSizedClass(panelFrame)
|
237
|
+
|
238
|
+
# If column contains nothing other than directives, remove it.
|
239
|
+
#
|
240
|
+
# Called only when a section has been fully laid out.
|
241
|
+
#
|
242
|
+
# Do the test on a clone, so we don't strip directives from actual column,
|
243
|
+
# which will still be checked by checkForDirectives.
|
244
|
+
#
|
245
|
+
# If column's frame is empty, remove that, too.
|
246
|
+
#
|
247
|
+
checkForEmpties: (column) ->
|
248
|
+
columnClone = column.clone()
|
249
|
+
columnClone.find(".alongslide").detach()
|
250
|
+
if @isEmpty columnClone
|
251
|
+
columnFrame = column.parent('.frame')
|
252
|
+
@log "Removing empty column from flow frame at " +
|
253
|
+
"#{@getPositionOf columnFrame}", @SUB_FRAME_LEVEL
|
254
|
+
column.detach()
|
255
|
+
|
256
|
+
# Reset entire section layout, as regionFlow has kept a record of this column
|
257
|
+
document.namedFlows.get(columnFrame.data('als-section-id')).resetRegions()
|
258
|
+
|
259
|
+
# destroy column frame if it's empty, too
|
260
|
+
@destroyFlowFrame columnFrame if @isEmpty columnFrame
|
261
|
+
|
262
|
+
# while we're at it, check if any empty frames were created (probably at the end)
|
263
|
+
@frames.children('.flow').find('.frame:empty').each (index, frame) =>
|
264
|
+
@destroyFlowFrame $(frame)
|
265
|
+
|
266
|
+
# Check for orphaned content. This can take many forms, so this method will
|
267
|
+
# grow and evolve as cases emerge.
|
268
|
+
#
|
269
|
+
checkForOrphans: (column) ->
|
270
|
+
# If column ends with a header, push it to the overflow column.
|
271
|
+
column.find(':last:header').detach().prependTo($('.'+@regionCls+':last'))
|
272
|
+
|
273
|
+
# Given a flow frame, find the next in line after it--or create one if none exists.
|
274
|
+
#
|
275
|
+
findOrBuildNextFlowFrame: (lastFrame) ->
|
276
|
+
nextFlowFrame = if lastFrame?.length then lastFrame.next('.frame')
|
277
|
+
unless nextFlowFrame?.length
|
278
|
+
nextFlowFrame = @buildFlowFrame lastFrame
|
279
|
+
return nextFlowFrame
|
280
|
+
|
281
|
+
# Build one frame to hold columns of flowing text.
|
282
|
+
#
|
283
|
+
# Only build new frame if there are none. Otherwise, clone the last one.
|
284
|
+
#
|
285
|
+
buildFlowFrame: (lastFrame) ->
|
286
|
+
position = if lastFrame?.length
|
287
|
+
@getPositionOf(lastFrame) + 1
|
288
|
+
else
|
289
|
+
@nextFramePosition()
|
290
|
+
@log "Building flow frame at #{position}", @FRAME_LEVEL
|
291
|
+
frame = if lastFrame?.length
|
292
|
+
lastFrame?.clone().empty()
|
293
|
+
else
|
294
|
+
$('<div class="frame"/>')
|
295
|
+
frame.appendTo @frames.children('.flow')
|
296
|
+
@setPositionOf frame, to: position
|
297
|
+
|
298
|
+
# Destroy frame, shifting any subsequent panels up by one.
|
299
|
+
#
|
300
|
+
destroyFlowFrame: (frame) ->
|
301
|
+
@log "Destroying flow frame at #{@getPositionOf frame}", @FRAME_LEVEL
|
302
|
+
frame.detach()
|
303
|
+
@frames.children('.panels').find('.panel.frame').each (index, panel) =>
|
304
|
+
panelPosition = $(panel).data(@IN_POINT_KEY)
|
305
|
+
if panelPosition > frame.data(@IN_POINT_KEY)
|
306
|
+
@log "Moving panel \"#{$(panel).data('alongslide-id')}\" at " +
|
307
|
+
"#{panelPosition} to #{panelPosition-1}", @FRAME_LEVEL
|
308
|
+
$(panel).data(@IN_POINT_KEY, panelPosition-1)
|
309
|
+
|
310
|
+
# Create region to receive flowing text (column). Return jQuery object.
|
311
|
+
#
|
312
|
+
buildRegion: (frame, flowName) ->
|
313
|
+
@log "Building column in flow frame at #{@getPositionOf frame}", @SUB_FRAME_LEVEL
|
314
|
+
region = $('<div/>').addClass(@regionCls)
|
315
|
+
frame.attr('data-als-section-id', flowName)
|
316
|
+
region.appendTo frame
|
317
|
+
document.namedFlows.get(flowName).addRegion(region.get(0))
|
318
|
+
return region
|
319
|
+
|
320
|
+
# Pull panel element out of @panels storage, apply its transition, and
|
321
|
+
# append to DOM!
|
322
|
+
#
|
323
|
+
# @param id - Alongslide panel ID
|
324
|
+
#
|
325
|
+
buildPanel: (id, position) ->
|
326
|
+
panel = @panels[id].clone().addClass('unstaged').show()
|
327
|
+
alignment = _.filter @ALIGNMENTS, (alignment) -> panel.hasClass(alignment)
|
328
|
+
@log "Building #{alignment} panel frame \"#{id}\" at position #{position}", @FRAME_LEVEL
|
329
|
+
panel.addClass('frame')
|
330
|
+
panel.appendTo @frames.children('.panels')
|
331
|
+
@setPositionOf panel, to: position
|
332
|
+
return panel
|
333
|
+
|
334
|
+
# Destroy all previously laid out content.
|
335
|
+
#
|
336
|
+
reset: ->
|
337
|
+
@laidOutLength = 0
|
338
|
+
|
339
|
+
@frames.find('.backgrounds').empty()
|
340
|
+
@frames.find('.flow').empty()
|
341
|
+
@frames.find('.panels').empty()
|
342
|
+
|
343
|
+
# remove regionFlows' internal record of regions we just destroyed
|
344
|
+
_.each document.namedFlows.namedFlows, (flow) -> flow.resetRegions()
|
345
|
+
|
346
|
+
# Write the given array of backgrounds to the DOM.
|
347
|
+
#
|
348
|
+
writeBackgrounds: ->
|
349
|
+
for background in @backgrounds
|
350
|
+
@frames.find('.backgrounds').append(background.clone())
|
351
|
+
|
352
|
+
# Set frame start/end position.
|
353
|
+
#
|
354
|
+
# @param options
|
355
|
+
# to: in point (= start position)
|
356
|
+
# until: out point (= end position)
|
357
|
+
#
|
358
|
+
setPositionOf: (frame, options={}) ->
|
359
|
+
frameType = frame.parent().get(0).className
|
360
|
+
if options.to?
|
361
|
+
if (currentFramePosition = @getPositionOf frame)?
|
362
|
+
@log "Moving #{frameType} frame at #{currentFramePosition} to " +
|
363
|
+
"#{options.to}", @SUB_FRAME_LEVEL
|
364
|
+
frame.data @IN_POINT_KEY, options.to
|
365
|
+
if options.until?
|
366
|
+
@log "Dismissing #{frameType} frame \"#{frame.data('alongslide-id')}\" " +
|
367
|
+
"at #{options.until}", @SUB_FRAME_LEVEL
|
368
|
+
frame.data @OUT_POINT_KEY, options.until
|
369
|
+
return frame
|
370
|
+
|
371
|
+
# Return start position.
|
372
|
+
#
|
373
|
+
getPositionOf: (frame) ->
|
374
|
+
frame.data(@IN_POINT_KEY)
|
375
|
+
|
376
|
+
# What position the _next_ (as yet unrendered) frame should be at.
|
377
|
+
#
|
378
|
+
nextFramePosition: ->
|
379
|
+
framePosition = @lastFramePosition()
|
380
|
+
if framePosition? then framePosition + 1 else 0
|
381
|
+
|
382
|
+
# Return the largest frame number of all frames.
|
383
|
+
#
|
384
|
+
lastFramePosition: ->
|
385
|
+
flowsAndPanels = @frames.children('.flow, .panels').find('.frame')
|
386
|
+
allFramePositions = _(flowsAndPanels).map (frame) => @getPositionOf $(frame)
|
387
|
+
return if allFramePositions.length then Math.max(allFramePositions...)
|
388
|
+
|
389
|
+
# Certain frame CSS classes limit the number of columns allowed
|
390
|
+
# per flow frame.
|
391
|
+
#
|
392
|
+
numFrameColumns: (frame) ->
|
393
|
+
numColumns = if frame.hasClass('three-columns') then 3 else 2
|
394
|
+
numColumns -= 1 if @isWithHorizontalPanel(frame)
|
395
|
+
numColumns -= 1 if frame.hasClass('three-columns') and frame.hasClass("with-panel-sized-two-thirds")
|
396
|
+
return numColumns
|
397
|
+
|
398
|
+
# Once render is done, build relevant indices for later lookup.
|
399
|
+
#
|
400
|
+
index: ->
|
401
|
+
@panelIndex = {}
|
402
|
+
@frames.children('.panels').find('.panel.frame').each (index, panel) =>
|
403
|
+
outPosition = $(panel).data(@OUT_POINT_KEY) || $(panel).data(@IN_POINT_KEY)
|
404
|
+
outPosition = @lastFramePosition() if outPosition is -1
|
405
|
+
for position in [$(panel).data(@IN_POINT_KEY)..outPosition]
|
406
|
+
@panelIndex[position] ?= []
|
407
|
+
@panelIndex[position].push panel
|
408
|
+
|
409
|
+
# Re-order elements in DOM if specified.
|
410
|
+
#
|
411
|
+
# This is a more reliable method of forcing a higher z-index for certain panels.
|
412
|
+
#
|
413
|
+
reorder: ->
|
414
|
+
frontPanels = @frames.children('.panels').find('.panel.front').detach()
|
415
|
+
@frames.children('.panels').append(frontPanels)
|
416
|
+
|
417
|
+
# DOM utility.
|
418
|
+
#
|
419
|
+
# Find background for a given section (flow).
|
420
|
+
#
|
421
|
+
findBackground: (flowName) ->
|
422
|
+
@frames.children('.backgrounds').find(".background.frame[data-alongslide-id=#{flowName}]")
|
423
|
+
|
424
|
+
# DOM utility.
|
425
|
+
#
|
426
|
+
findPanel: (id) ->
|
427
|
+
@frames.children('.panels').find(".panel.frame[data-alongslide-id=#{id}]")
|
428
|
+
|
429
|
+
# Check panel index for panel at given position, and check if it's horizontal.
|
430
|
+
#
|
431
|
+
horizontalPanelAt: (position, edge) ->
|
432
|
+
edges = if edge? then [edge] else @HORIZONTAL_EDGES
|
433
|
+
_(@panelIndex[position] || []).any (panel) ->
|
434
|
+
_(edges).any (edge) -> $(panel).hasClass(edge)
|
435
|
+
|
436
|
+
# Check if flow frame shares space with horizontally-pinned panel.
|
437
|
+
#
|
438
|
+
isWithHorizontalPanel: (frame) ->
|
439
|
+
for cssClass in _.map(@HORIZONTAL_EDGES, (edge) -> "with-panel-pinned-#{edge}")
|
440
|
+
return true if frame.hasClass(cssClass)
|
441
|
+
|
442
|
+
# Given a directive for a pinned panel, retun the class to be applied to
|
443
|
+
# flow frames.
|
444
|
+
#
|
445
|
+
withPinnedClass: (directive) ->
|
446
|
+
edge = _.first _.filter @EDGES, (edge) -> directive.hasClass(edge)
|
447
|
+
"with-panel-pinned-#{edge}"
|
448
|
+
|
449
|
+
# Given a directive for a sized panel, retun the class to be applied to
|
450
|
+
# flow frames.
|
451
|
+
# TODO: maybe combine withPinnedClass and withSizedClass
|
452
|
+
withSizedClass: (directive) ->
|
453
|
+
size = _.first _.filter @SIZES, (size) -> directive.hasClass(size)
|
454
|
+
size ?= "half"
|
455
|
+
"with-panel-sized-#{size}"
|
456
|
+
|
457
|
+
# If panel is partial width (i.e. is with left/right pinned panel),
|
458
|
+
# then return its scrolling scale (0.0-1.0).
|
459
|
+
#
|
460
|
+
# Called by Scrolling to determine scoll distance.
|
461
|
+
#
|
462
|
+
# @return partialWidth - a percentage width (0.0-1.0), or undefined.
|
463
|
+
# Note: This number doesn't have to be exact--just has to "feel" right for
|
464
|
+
# pinned panels of different widths.
|
465
|
+
#
|
466
|
+
framePartialWidth: (frame) ->
|
467
|
+
if @isWithHorizontalPanel(frame)
|
468
|
+
$column = frame.find('.'+@regionCls)
|
469
|
+
return ($column.width() + $column.position().left) * @numFrameColumns(frame) / frame.width()
|
470
|
+
|
471
|
+
# Return panel alignment.
|
472
|
+
#
|
473
|
+
panelAlignment: (directive) ->
|
474
|
+
_.first _.filter @ALIGNMENTS, (alignment) -> directive.hasClass(alignment)
|
475
|
+
|
476
|
+
# Returns true when all flowing text has been laid out (i.e. last column
|
477
|
+
# region no longer contains overflow.)
|
478
|
+
#
|
479
|
+
flowComplete: (flowName) ->
|
480
|
+
not document.namedFlows.get(flowName).updateOverset()
|
481
|
+
|
482
|
+
# Utility: Is column empty after we remove directives from it?
|
483
|
+
#
|
484
|
+
isEmpty: (el) ->
|
485
|
+
$.trim(el.children().html()) == ''
|
486
|
+
|
487
|
+
# Compute length of total laid out content so far and broadcast it to our
|
488
|
+
# listeners.
|
489
|
+
#
|
490
|
+
# To listen:
|
491
|
+
#
|
492
|
+
# $(document).on 'alongslide.progress', (e, progress) ->
|
493
|
+
# #
|
494
|
+
#
|
495
|
+
updateProgress: (newElement) ->
|
496
|
+
@laidOutLength += newElement.text().length
|
497
|
+
$(document).triggerHandler 'alongslide.progress', (@laidOutLength / @sourceLength)
|
498
|
+
|
499
|
+
# Write debug log to console if available/desired.
|
500
|
+
#
|
501
|
+
log: (message, indentLevel = 0) ->
|
502
|
+
indent = (_(indentLevel).times -> ". ").join('')
|
503
|
+
if console? and @debug
|
504
|
+
console.log "#{indent}#{message} (elapsed: #{(new Date - @startTime).valueOf()}ms)"
|
@@ -0,0 +1,125 @@
|
|
1
|
+
#
|
2
|
+
# parser.coffee: parse raw HTML into Alongslide types: sections, panels, etc.
|
3
|
+
#
|
4
|
+
# Copyright 2013 Canopy Canopy Canopy, Inc.
|
5
|
+
# Author Adam Florin
|
6
|
+
|
7
|
+
class Alongslide::Parser
|
8
|
+
|
9
|
+
# Store names of flows here as we create them.
|
10
|
+
#
|
11
|
+
# sections: []
|
12
|
+
backgrounds: []
|
13
|
+
flowNames: []
|
14
|
+
|
15
|
+
constructor: (options) ->
|
16
|
+
{@source} = options
|
17
|
+
@preprocessSource()
|
18
|
+
|
19
|
+
#
|
20
|
+
#
|
21
|
+
preprocessSource: ->
|
22
|
+
# Put dummy content inside empty directives as CSSRegions trims any empty
|
23
|
+
# elements found near the boundaries of a region.
|
24
|
+
(@source.find ".alongslide:empty").text("[ALS]")
|
25
|
+
|
26
|
+
# Parser entrypoint.
|
27
|
+
#
|
28
|
+
# Build sections and store them directly as CSSRegions named flows.
|
29
|
+
#
|
30
|
+
# Retun panels and footnotes, which will be needed by other components.
|
31
|
+
#
|
32
|
+
# Note! Parse order matters! Sections should go last, once all non-section
|
33
|
+
# material has been scraped out of @source.
|
34
|
+
#
|
35
|
+
parse: ->
|
36
|
+
@sourceLength = 0
|
37
|
+
|
38
|
+
panels = @collectPanels()
|
39
|
+
footnotes = @collectFootnotes()
|
40
|
+
@collectSections()
|
41
|
+
|
42
|
+
flowNames: @flowNames
|
43
|
+
backgrounds: @backgrounds
|
44
|
+
panels: panels
|
45
|
+
footnotes: footnotes
|
46
|
+
sourceLength: @sourceLength
|
47
|
+
|
48
|
+
#
|
49
|
+
#
|
50
|
+
collectPanels: ->
|
51
|
+
rawPanels = @source.find('.alongslide.show.panel')
|
52
|
+
|
53
|
+
_.object _.map rawPanels, (el) ->
|
54
|
+
$el = $(el)
|
55
|
+
panelId = $el.data('alongslide-id')
|
56
|
+
panelEl = $el.clone().removeClass('show')
|
57
|
+
|
58
|
+
# Cleanup
|
59
|
+
$el.empty().removeClass('panel')
|
60
|
+
|
61
|
+
return [ panelId, panelEl ]
|
62
|
+
|
63
|
+
# Sift through passed-in sections, delineating them based on `enter` and `exit`
|
64
|
+
# directives, then assigning each to a flow.
|
65
|
+
#
|
66
|
+
collectSections: ->
|
67
|
+
@source.find('.alongslide.enter.section').each (index, directiveElement) =>
|
68
|
+
directive = $(directiveElement)
|
69
|
+
id = directive.data('alongslide-id')
|
70
|
+
exitSelector = '.alongslide.exit.section[data-alongslide-id='+id+']'
|
71
|
+
|
72
|
+
# build section for content BEFORE section enter
|
73
|
+
lastSectionContents = directive.prevAll().detach().get().reverse()
|
74
|
+
@buildSection(lastSectionContents, directive) if lastSectionContents.length
|
75
|
+
|
76
|
+
# build section for content AFTER section enter
|
77
|
+
sectionContents = directive.nextUntil(exitSelector).detach()
|
78
|
+
@buildSection(sectionContents, directive, id)
|
79
|
+
|
80
|
+
# cleanup section build process
|
81
|
+
directive.remove()
|
82
|
+
(@source.find exitSelector).remove()
|
83
|
+
|
84
|
+
@buildSection @source.children() unless @source.is(':empty')
|
85
|
+
|
86
|
+
# Build section, given content.
|
87
|
+
#
|
88
|
+
# Create new NamedFlow for it, and log the name.
|
89
|
+
#
|
90
|
+
# Create section background.
|
91
|
+
#
|
92
|
+
# @param content - jQuery object of section contents
|
93
|
+
# @param directive (optional) - directive which specified the section
|
94
|
+
# @param id (optional) - Alongslide section ID
|
95
|
+
#
|
96
|
+
buildSection: (content, directive, id) ->
|
97
|
+
flowName = id || "sectionFlow#{@flowNames.length}"
|
98
|
+
|
99
|
+
idElement = directive?.removeClass('alongslide').empty().removeClass('enter') || $("<div/>")
|
100
|
+
idElement.attr('data-alongslide-id', flowName)
|
101
|
+
|
102
|
+
# create section
|
103
|
+
section = idElement.clone().append(content)
|
104
|
+
|
105
|
+
# tally up length
|
106
|
+
@sourceLength += section.text().length
|
107
|
+
|
108
|
+
# create NamedFlow
|
109
|
+
document.namedFlows.get(flowName).addContent(section.get(0))
|
110
|
+
@flowNames.push flowName
|
111
|
+
|
112
|
+
# create background if ID is specified
|
113
|
+
if id?
|
114
|
+
background = idElement.clone().addClass('background frame').html(" ")
|
115
|
+
@backgrounds.push(background)
|
116
|
+
|
117
|
+
# Search for footntes as formatted by Redcarpet.
|
118
|
+
#
|
119
|
+
# Each has an ID of the form `fn1`, which corresponds to the links in the
|
120
|
+
# footnote references.
|
121
|
+
#
|
122
|
+
# Returns a jQuery list of li elements and removes the generated footnotes from DOM
|
123
|
+
collectFootnotes: ->
|
124
|
+
@source.find('.footnotes:last-child')
|
125
|
+
.remove()
|