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.
@@ -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("&nbsp;")
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()