Skip to content

Instantly share code, notes, and snippets.

@mrjjwright
Created June 13, 2011 02:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mrjjwright/1022241 to your computer and use it in GitHub Desktop.
Save mrjjwright/1022241 to your computer and use it in GitHub Desktop.
Bus.js
# Bus 0.1(alpha)
# (c) 2010 John Wright, QuickLeft Inc.
# Bus may be freely distributed under the MIT license.
# For all details and documentation:
# http://github.com/mrjjwright/Bus
#
#
#
# Bus would not be possible without Jeremy Ashkenas who wrote CoffeeScript, the language
# in which Bus is written, Underscore, and Backbone.js, all which Bus uses or borrows from heavily.
# Save a reference to the global object.
root = this
# Save the previous value of the `Bus` variable.
previousBus = root.Bus
# The top-level namespace. All public Bus classes and modules will
# be attached to this. Exported for both CommonJS and the browser.
if exports? then Bus = exports
else Bus = root.Bus = {}
# Current version of the library. Keep in sync with `package.json`.
Bus.VERSION = '0.1-pre'
# Require Underscore, if we're on the server, and it's not already present.
_ = root._
if (!_ and require?) then _ = require('underscore')._
# For Bus's purposes, jQuery or Zepto owns the `$` variable.
$ = root.jQuery || root.Zepto
# Runs Bus.js in *noConflict* mode, returning the `Bus` variable
# to its previous owner. Returns a reference to this Bus object.
Bus.noConflict = ->
root.Bus = previousBus
return this
# Bus.Events * stolen from Backbone.Events and ported to CoffeeScript
# -----------------
# A module that can be mixed in to *any object* in order to provide it with
# custom events. You may `bind` or `unbind` a callback function to an event
# `trigger`-ing an event fires all callbacks in succession.
#
# var object = {}
# _.extend(object, Bus.Events)
# object.bind('expand', function(){ alert('expanded') })
# object.trigger('expand')
#
class Bus.Events
# Bind an event, specified by a string name, `ev`, to a `callback` function.
# Passing `"all"` will bind the callback to all events fired.
bind: (ev, callback) ->
calls = @_callbacks or (@_callbacks = {})
list = calls[ev] or (calls[ev] = [])
list.push(callback)
return @
# Remove one or many callbacks. If `callback` is null, removes all
# callbacks for the event. If `ev` is null, removes all bound callbacks
# for all events.
unbind: (ev, callback) ->
if not ev
@_callbacks = {}
else if (calls = @_callbacks)
if (!callback)
calls[ev] = []
else
list = calls[ev]
if not list then return @
for i in [0..list.length]
if callback is list[i]
list[i] = null
break
return @
# Trigger an event, firing all bound callbacks. Callbacks are passed the
# same arguments as `trigger` is, apart from the event name.
# Listening for `"all"` passes the true event name as the first argument.
trigger: (eventName) ->
both = 2
if not (calls = @._callbacks) then return @
while both--
ev = if both then eventName else 'all'
if list = calls[ev]
l = list.length
for i in [0..l]
unless (callback = list[i])
list.splice(i, 1)
i--
l--
else
args = if both then Array.prototype.slice.call(arguments, 1) else arguments
callback.apply(@, args)
return @
# Pubsub semantics for bind/unbind/trigger
Bus.publish = Bus.Events.prototype.trigger
Bus.subscribe = Bus.Events.prototype.bind
Bus.unsubscribe = Bus.Events.prototype.unbind
# Bus Core methods
# Start the bus
Bus.start = () ->
bindBusLinks()
bindStaticLinks()
# Handle buttons or hrefs intended to control view controllers
bindBusLinks = ->
$("a.push-vc, button.push-vc").live 'click', (event) ->
event.preventDefault()
vcid = $(this).attr('href')
vcid = vcid or $(this).attr('data-vcid')
if Bus.hasViewController(vcid)
Bus.pushViewController(vcid)
else
Bus.publish('vc/missing', vcid)
return false
$("a.pop-vc, button.pop-vc").live 'click', (event) ->
event.preventDefault()
vcid = $(this).attr('href')
vcid = vcid or $(this).attr('data-vcid')
if Bus.hasViewController(vcid)
Bus.popViewController(vcid)
console.log('got here')
return false
$("a.show-vc, button.pop-vc").live 'click', (event) ->
event.preventDefault()
vcid = $(this).attr('href')
vcid = vcid or $(this).attr('data-vcid')
if Bus.hasViewController(vcid)
Bus.showViewController(vcid)
else
Bus.publish('vc/missing', vcid)
return false
# If hrefs have class of 'static', load their
# href into dom element with id of the attr 'data-target-id'
bindStaticLinks = ->
$('a.static').live 'click', (e) ->
target = $(this).attr('data-target-id')
$("##{target}").load $(this).attr('href') if target
e.preventDefault()
return false
# View Controller management
Bus.pushSupport = root.history? and root.history.pushState?
# we store a viewController stack if the env doesn't have pushState support
Bus.viewControllerStack = []
# A map of every view controller managed by the bus
Bus.viewControllers = {}
if Bus.pushSupport
window.onpopstate = (event) ->
# window.afterFirstLoad is a hack to make sure that this function is not called twice on page load
if event.state? and window.afterFirstLoad
Bus.popViewController(event.state)
window.afterFirstLoad = true
# Push a view controller onto the viewController stack and show it
Bus.pushViewController = (viewController, replace) ->
if _.isString(viewController)
vcid = viewController
viewController = Bus.viewControllers[vcid] or null
else vcid = viewController.vcid()
throw 'Missing viewController' unless viewController?
throw 'Missing vcid' unless vcid?
if Bus.pushSupport
if replace then window.history.replaceState(vcid, document.title, url)
else window.history.pushState(vcid, document.title, "/#{vcid}")
else
Bus.viewControllerStack.push(vcid)
viewController.show()
Bus.publish("vc/pushed", viewController.vcid())
# pops a view controller from the viewController stack and hides it
Bus.popViewController = (state) ->
state = state ? Bus.viewController.stack.pop()
return unless state.vcid?
viewController = Bus.viewControllers[state.vcid]
viewController.hide()
# Show a viewController and alert the bus. Pass a vcid or a viewController.
Bus.showViewController = (viewController) ->
if _.isString(viewController)
vcid = viewController
viewController = Bus.viewControllers[vcid] or null
else vcid = viewController.vcid()
throw 'Missing viewController' unless viewController?
throw 'Missing vcid' unless vcid?
viewController.show()
Bus.registerViewController = (viewController) ->
throw 'Must supply vcid' unless viewController and viewController.vcid?
Bus.viewControllers[viewController.vcid()] = viewController
Bus.hasViewController = (viewController) ->
vcid = if _.isString(viewController) then viewController else viewController.vcid()
return Bus.viewControllers[vcid]?
#
# Bus.View
#
# Simply Bus's View ported to CoffeeScript with some unnecssary stuff removed
class Bus.View
constructor: (@options) ->
@._configure(options or {})
@.initialize(options)
@addStyle()
# Static props
@selectorDelegate: (selector) ->
$(selector, @el)
# Cached regex to split keys for `delegate`.
@eventSplitter = /^(\w+)\s*(.*)$/
# Attach the `selectorDelegate` function as the `$` property.
$: View.selectorDelegate
# Initialize is an empty function by default. Override it with your own
# initialization logic.
initialize : ->
# **render** is the core function that your view should override. The
# convention is for **render** to always return `this`.
render: ->
return @
# Uses underscore.css to add style
addStyle: ->
if @style and not @styleLoaded
$("<style type='text/css'>#{_.toCSS(@style)}</style>").appendTo("head")
@styleLoaded = true
# Set callbacks, where `@callbacks` is a hash of
#
# *{"event selector": "callback"}*
#
# {
# 'mousedown .title': 'edit',
# 'click .button': 'save'
# }
#
# pairs. Callbacks will be bound to the view, with `this` set properly.
# Uses event delegation for efficiency.
# Omitting the selector binds the event to `@el`.
# This only works for delegate-able events: not `focus`, `blur`, and
# not `change`, `submit`, and `reset` in Internet Explorer.
# Warning, unlike Backbone.View delegateEvents this method does not unbind
# events on the current element before delegating the new ones.
# This is so view controllers and views can both respond to events
delegateEvents: (obj, events) ->
obj = obj || this
events = events || @events
el = @el
return unless events
return unless el
for key of events
methodName = events[key]
match = key.match(View.eventSplitter)
eventName = match[1]
selector = match[2]
method = _.bind(obj[methodName], obj)
if selector is ''
$(el).bind(eventName, method)
else
$(el).delegate(selector, eventName, method)
# Performs the initial configuration of a View with a set of options.
_configure: (options) ->
if (@options) then options = _.extend({}, @options, options)
if (options.el) then @el = options.el
@options = options
# Bus.ViewController
# --------------
class Bus.ViewController
constructor: ->
@active = false
# get a view by default
@view = new Bus.View()
@initialize()
Bus.registerViewController(@)
# Subclasses should override to publish and subscribe to the bus and other initialization
initialize: ->
# Display the view controller
show: ->
@active = true
@renderView()
Bus.publish('vc/show', @vcid)
# Subclasses should call this from renderView if active.
# View controllers also can overwrite this in order to attach their views to the dom any way they want.
# By default this function will cause the implicit view's el to occupy its $parent, taking up it's whole html.
attachView: ->
if @view and @view.$parent and @view.el
@inactivateNodeResident(@view.$parent)
@view.$parent.html(@view.el)
@occupyNode(@view.$parent)
@.delegateEvents()
# Inactive the current view controller that is rendering into this node, if one
inactivateNodeResident: ($node) ->
currentVcid = $node.attr('data-vcid')
currentViewController = Bus.viewControllers[currentVcid] if currentVcid
currentViewController.active = false if currentViewController
# Set the vcid on the suppled node as a data attr
occupyNode: ($node) ->
$node.attr('data-vcid', @vcid())
# Convenience function to delegate events on the implicit view to the view controller
delegateEvents: (events) ->
events = events || @events
return unless events
return unless @view
@view.delegateEvents(@, events)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment