app/scripts/ui-lib/widgets/carousel.coffee

Carousel

The Carousel class provides a carousel widget.

author: Julien Ramboz version: 1.0 references: WAI-ARIA listbox role, AOL's Listbox style guide](http://access.aol.com/dhtml-style-guide-working-group/#listbox) usage: Carousel examples
requires: List

AMD loader

Try loading as AMD module or fall back to default loading

((widget) -> if typeof define is "function" and define.amd define ["jslib", "core", "widget", "list"], widget else widget @$, _, _.AbstractWidget, _.List ) carousel = ($, _, AbstractWidget, List) -> "use strict"

Widget

The actual widget class

class Carousel extends List

Default options for the widget

@defaultOptions:
  • event: the event to react to
event: "click"
  • panels: a selector pointing to the panels
panels: ""
  • items: a selector pointing to the items (from within the panels)
items: ""
  • controls: how to display the controls (false: do not display, true or after: display after the panels, before: display before the panels, otherwise use a selector to find them)
controls: "false"
  • prev: whether to display the previous button or not, or the selector to the button
prev: "true"
  • next: whether to display the next button or not, or the selector to the button
next: "true"
  • selectors: display the selectors (true, false or a selector pointing to the selectors)
selectors: "true"
  • cycleItems: whether to cycle though the items or not (true or false)
cycleItems: false
  • orientation: orientation of the carousel (horizontal or vertical)
orientation: 'horizontal'

Initialisation

Initializer function.

initialize: (options) -> super options @controls = @findControls() @panels = @findPanels() @items = @findItems() @prevButton = @findPrevious() @nextButton = @findNext() @selectors = @findSelectors() @selectionIndicator = @createSelectionIndicator()

Accessibility markup

Add aria attributes

aria: () -> @element.attr("data-role", "carousel") if (@controls) @prevButton?.attr("data-role", "previous") @nextButton?.attr("data-role", "next") super() @wrapper.prepend @selectionIndicator @selectionIndicator.attr 'role', 'presentation' @element.find('[role="listbox"]').prepend(@selection) if @controls and @selectors @selectors.attr 'role', 'presentation' if @controls and @controls.get(0).getAttribute("data-widget") isnt "toolbar" @controls["#{_.namespace}toolbar"]()

Event handling

Attach evenets to the widget

bindEvents: () -> super()

show previous panel when clicking previous button

@element.on @options.event, "[data-role='previous']", (e) => if @prevButton.get(0).getAttribute("aria-disabled") isnt "true" e.preventDefault() @keyPageUp(e)

show next panel when clicking next button

@element.on @options.event, "[data-role='next']", (e) => if @nextButton.get(0).getAttribute("aria-disabled") isnt "true" e.preventDefault() @keyPageDown(e) @selectItem()

Select the specified item

selectItem: ($item) -> return if @element.get(0).getAttribute("aria-disabled") is "true" if @currentItem and @currentItem.attr('data-target') $target = $(@currentItem.attr('data-target')) if $target.attr('src') isnt undefined $target.attr('src', '') @currentItem = $item or @items.first() @currentPanel = @panels.filter( (i) => @panels.eq(i).find(@currentItem).length ) @element.trigger("beforeSelect", {item: @currentItem, panel: @currentPanel}) @items.removeAttr("tabindex") if @controls @prevButton?.attr "aria-disabled": @panels.get(0) is @currentPanel[0] and not @options.cycleItems "tabindex": if @panels.get(0) is @currentPanel[0] and not @options.cycleItems "-1" else "0" @nextButton?.attr "aria-disabled": @panels.get(-1) is @currentPanel[0] and not @options.cycleItems "tabindex": if @panels.get(-1) is @currentPanel[0] and not @options.cycleItems "-1" else "0" @panels.filter( (i) => @panels.get(i).getAttribute("aria-hidden") isnt "true") .attr("aria-hidden", "true") @currentPanel.attr("aria-hidden", "false") @currentItem.attr("tabindex", 0) @element.trigger("afterSelect", {item: @currentItem, panel: @currentPanel}) @wrapper.attr("aria-activedescendant", @currentItem.attr("id")) if @options.orientation is 'vertical' @selectionIndicator.css('top', @currentItem.position().top + @currentItem.height() / 2) else @selectionIndicator.css('left', @currentItem.position().left + @currentItem.width() / 2) if @currentItem.attr('data-target') $target = $(@currentItem.attr('data-target')) if $target.attr('src') isnt undefined $target.attr('src', @currentItem.attr('data-url')) if @selectors @selectors.find('[data-role="selector-index"]').text( @items.index(@currentItem) + 1) @selectors.find('[data-role="selector-count"]').text(@items.length) @currentItem

Structure discovery

Find the widget structure using the specified configuration

Controls

Find the controls using:

  • element with role="toolbar"
  • controls options attribute (before, after,true,false` or css selector)
  • second child if controls are positioned after
  • first child otherwise
  • auto-generated
findControls: () -> return if not @options.controls or @options.controls is "false" $controls = $("[role='toolbar']", @element) return $controls if $controls.length if @element.children().length < 2 if @options.controls is "before" @createControls().prependTo(@element) else @createControls().appendTo(@element) else if @options.controls is "before" @element.children().eq(0) else if @options.controls is "after" or @options.controls is "true" or @options.controls is true @element.children().eq(1) else $(@options.controls, @element)

Previous control

Find the previous control using:

  • element with data-role="previous" (relative to the controls)
  • prev options (selector relative to the controls)
  • first child of the second child if controls are positioned after
  • first child of the controls
  • auto-generated
findPrevious: () -> return if not @controls or not @controls.length return if not @options.prev or @options.prev is "false" $prev = @controls.find("[data-role='previous']") return $prev if $prev.length if @options.prev is "true" or @options.prev is true return @createPrevious().appendTo(@controls) $prev = $(@options.prev, @controls) return $prev if $prev.length @controls.children().eq(0)

Next control

Find the next control using:

  • element with data-role="next" (relative to the controls)
  • next option (selector relative to the controls)
  • second child of the controls
  • auto-generated
findNext: () -> return if not @controls or not @controls.length return if not @options.next or @options.next is "false" $next = @controls.find("[data-role='next']") return $next if $next.length if @options.next is "true" or @options.next is true return @createNext().appendTo(@controls) $next = $(@options.next, @controls) return $next if $next.length @controls.children().eq(1)

Find the selectors using:

  • elements with data-role="selector" (relative to the controls)
  • selectors attribute (selector relative to the controls)
  • third child of the controls
  • auto-generated
findSelectors: () -> return if not @controls or not @controls.length $selectors = @controls.find("[data-role='selector']") return $selectors if $selectors.length if @options.selectors is "true" or @options.selectors is true return @createSelectors().appendTo(@controls) $selectors = $(@options.selectors, @controls) if $selectors.length $selectors else $prev = @prevButton and @prevButton.get(0) $next = @nextButton and @nextButton.get(0) @controls.children().filter( () -> this isnt $prev and this isnt $next) .first().children()

Structure creation

Create elements required by the structure if they are not present

Controls

createControls: () -> $(document.createElement("div"))

Previous control

createPrevious: () -> $(document.createElement("span"))

Next control

createNext: () -> $(document.createElement("span"))

Selectors

createSelectors: () -> $selector = $(document.createElement("div")).attr("data-role","selector") $selector.html('&nbsp;/&nbsp;') $selector.prepend( $(document.createElement("strong")).attr("data-role","selector-index")) $selector.append( $(document.createElement("span")).attr("data-role","selector-count"))

Selection indicator

createSelectionIndicator: () -> $(document.createElement('div')).attr 'data-role': 'carousel-selection-indicator'

Key handling

Handle enter key press

keyEnter: (e) -> if e.target.getAttribute("role") is "button" and e.target.getAttribute("aria-disabled") isnt "true" if e.target is @prevButton.get(0) and @currentItem[0] isnt @items.get(0) @keyPageUp(e) @prevButton.focus() else if e.target is @nextButton.get(0) and @currentItem[0] isnt @items.get(-1) @keyPageDown(e) @nextButton.focus()

Handle page up keypress.

keyPageUp: (e) ->

Cycle if at the begining of the carousel and option is activated

if @currentItem[0] is @items.get(0) and @options.cycleItems @selectItem(@items.last()).focus() else super(e)

Handle page down keypress.

keyPageDown: (e) ->

Cycle if at the end of the carousel and option is activated

if @currentItem[0] is @items.get(-1) and @options.cycleItems @selectItem(@items.first()).focus() else super(e)

Handle left key press

keyLeft: (e) -> return if @prevButton and @prevButton.get(0) is e.target return if @nextButton and @nextButton.get(0) is e.target super(e)

Handle up key press

keyUp: (e) -> if not (@controls and @controls.find($(e.target)).length) e.preventDefault() @keyLeft(e)

Handle right key press

keyRight: (e) -> return if @prevButton and @prevButton.get(0) is e.target return if @nextButton and @nextButton.get(0) is e.target super(e)

Handle down key press

keyDown: (e) -> if not (@controls and @controls.find($(e.target)).length) e.preventDefault() @keyRight(e)

Cleanup

Cleanup the widget and remove remaining references

destructor: () -> super

Installation

Install the widget into JS library

Carousel.install("Carousel", () -> $("[data-widget='carousel']")["#{_.namespace}carousel"]() )