abe33/awesome_form

View on GitHub
app/assets/javascripts/awesome_ui/widgets.js.coffee

Summary

Maintainability
Test Coverage


#
__widgets__ = {}

# The `__instances__` object stores the returned instances of the various widgets,
# stored by widget type and then mapped with their target DOM element as key.
__instances__ = {}

## widgets

# The `widgets` function is both the main module and the function
# used to register the widgets to apply on a page.
widgets = (name, selector, options={}, block) ->
  unless __widgets__[name]?
    throw new Error "Unable to find widget '#{name}'"

  # The options specific to the widget registration and activation are
  # extracted from the options object.
  events = options.on or 'init'
  if_condition = options.if
  unless_condition = options.unless
  media_condition = options.media

  delete options.on
  delete options.if
  delete options.unless
  delete options.media

  # Events can be passed as a string with event names separated with spaces.
  events = events.split /\s+/g if typeof events is 'string'

  # The widgets instances are stored in a Hash with the DOM element they
  # target as key. The instances hashes are stored per widget type.
  instances = __instances__[name] ||= new widgets.Hash

  # This method execute a test condition for the given element. The condition
  # can be either a function or a value converted to boolean.
  test_condition = (condition, element) ->
    if typeof condition is 'function' then condition(element) else !!condition

  # The DOM elements handled by a widget will receive a handled class
  # to differenciate them from unhandled elements.
  handled_class = "#{name}-handled"

  # This method will test if an element can be handled by the current widget.
  # It will test for both the handled class presence and the widget
  # conditions. Note that if both the `if` and `unless` conditions
  # are passed in the options object they will be tested as both part
  # of a single `&&` condition.
  can_be_handled = (element) ->
    res = element.className.indexOf(handled_class) is -1
    res &&= test_condition(if_condition, element) if if_condition?
    res &&= not test_condition(unless_condition, element) if unless_condition?
    res

  # If a media condition have been specified, the widget activation will be
  # conditionned based on the result of this condition. The condition is
  # verified each time the `resize` event is triggered.
  if media_condition?
    # The media condition can be either a boolean value, a function, or,
    # to simply the setup, an object with `min` and `max` property containing
    # the minimal and maximal window width where the widget is activated.
    if typeof media_condition is 'object'
      {min, max} = media_condition
      media_condition = ->
        res = true
        res &&= window.innerWidth >= min if min?
        res &&= window.innerWidth <= max if max?
        res

    # The media handl is registered on the `resize` event of the `window`
    # object.
    media_handler = (element, widget) ->
      return unless widget?

      condition_matched = test_condition(media_condition, element)

      if condition_matched and not widget.active
        widget.activate?()
      else if not condition_matched and widget.active
        widget.deactivate?()

    window.addEventListener 'resize', ->
      instances.each_pair (element, widget) ->
        media_handler element, widget

  # The `handler` function is the function registered on specified event and
  # will proceed to the creation of the widgets if the conditions are met.
  handler = ->
    elements = document.querySelectorAll selector

    Array::forEach.call elements, (element) ->
      return unless can_be_handled element

      res = __widgets__[name] element, Object.create(options), elements
      element.className += " #{handled_class}"
      instances.set element, res

      # The widgets activation state are resolved at creation
      media_handler(element, res) if media_condition?

      block?.call element, element, res

  # For each event specified, the handler is registered as listener.
  # A special case is the `init` event that simply mean to trigger the
  # handler as soon a the function is called.
  events.forEach (event) ->
    switch event
      when 'init' then handler()
      when 'load', 'resize'
        window.addEventListener event, handler
      else
        document.addEventListener event, handler

#### widgets.define

# The `widgets.define` is used to create a new widget usable through the
# `widgets` method. Basically, a widget is defined using a `name`, and a
# `block` function that will be called for each DOM elements targeted by
# the widget.
#
# The `block` function should have the following signature:
#
#     function(element:HTMLElement, options:Object):Object
#
# The `options` object will contains all the options passed to the `widgets`
# method except the `on`, `if`, `unless` and `media` ones.
widgets.define = (name, block) -> __widgets__[name] = block

#### widgets.release

# The `widgets.release` method can be used to completely remove the widgets
# of the given `name` from the page.
# It's the widget responsibility to clean up its dependencies during
# the `dispose` call.
widgets.release = (name) ->
  __instances__[name].each (value) -> value?.dispose?()

#### widgets.activate

# Activates all the widgets instances of type `name`.
widgets.activate = (name) ->
  __instances__[name].each (value) -> value?.activate?()

#### widgets.deactivate

# Deactivates all the widgets instances of type `name`.
widgets.deactivate = (name) ->
  __instances__[name].each (value) -> value?.deactivate?()

window.widgets = widgets
window.widget = widgets
window.$w = widgets