Last active
December 16, 2015 13:48
-
-
Save lukemorton/5444235 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A class for controlling history state | |
# | |
# The aim of this class is to provide ways of pushing and | |
# replacing state by providing an action alongside a state. | |
# This approach differs from routing based solutions that | |
# provide functionality to make and detect state changes then | |
# handle them elsewhere. | |
# | |
# $button = $('.button') | |
# | |
# button_state = new StateController | |
# | |
# button_state.sync_push '/green', -> | |
# $button.css('background-color', 'green') | |
# | |
# button_state.push '/waiting', (done) -> | |
# setTimeout(done, 4000) | |
# | |
# button_state.sync_replace '/red', -> | |
# $button.css('background-color', 'red') | |
# | |
class StateController | |
@uid = 0 | |
# Set up a `StateController` instance with an optional | |
# object. Valid keys: | |
# | |
# - **auto**, automatically listen for `onpopstate` changes | |
# when instance created. Defaults to `true`. | |
# - **id**, a unique identifier for this `StateController` | |
# instance. Used for only responding to `onpopstate` | |
# events that relate to this controller. Defaults to an | |
# auto incremented number. | |
constructor: (s = {}) -> | |
@id = s.id or ++StateController.uid | |
@stack = [] | |
@queue = [] | |
@listening = no | |
@processing = no | |
@listen() unless s.auto is false | |
# Listen to window.onpopstate events specifically targeted | |
# at a `StateController` instance | |
listen: -> | |
return if @listening | |
@listening = yes | |
window.onpopstate = (event) => | |
# We return here if no event.state since we do not want | |
# to trigger any callbacks on page load. Everyone but | |
# Firefox triggers on page load, which means things are | |
# inconsistent, hence we don't bother. | |
return unless event.state? | |
{id, stack_id} = event.state | |
return unless id is @id | |
queue_handler(@, @stack[stack_id]) | |
process_queue = (sc) -> | |
return if sc.processing | |
sc.processing = yes | |
unless handler = sc.queue.shift() | |
sc.processing = no | |
return | |
handler -> | |
sc.processing = no | |
process_queue(sc) | |
queue_handler = (sc, handler) -> | |
sc.queue.push(handler) | |
process_queue(sc) | |
# Update state with a particular method (`'push'` or | |
# `'replace'`) and state object. Then queues the handler | |
# associated with this state. | |
update_state: (method, state, handler) -> | |
state.id = @id | |
state.stack_id = @stack.length | |
history["#{method}State"](state, state.title or '', state.uri) | |
@stack.push(handler) | |
queue_handler(@, handler) | |
return | |
# Replace the current state with a new URI and handler. The | |
# handler will be passed one argument, a callback which must | |
# be triggered when the state behaviour is finished. | |
replace: (uri, handler) -> @update_state('replace', uri: uri, handler) | |
# Push a new state with a URI and handler. The handler will | |
# be passed one argument, a callback which must be triggered | |
# when the state behaviour is finished. | |
push: (uri, handler) -> @update_state('push', uri: uri, handler) | |
wrap_sync = (handler) -> | |
return (done) -> | |
handler() | |
done() | |
# A synchronous form of `replace()`. The only difference is | |
# the handler is not passed any arguments and is expected to | |
# complete synchronously. | |
sync_replace: (uri, handler) -> @replace(uri, wrap_sync(handler)) | |
# A synchronous form of `push()`. The only difference is | |
# the handler is not passed any arguments and is expected to | |
# complete synchronously. | |
sync_push: (uri, handler) -> @push(uri, wrap_sync(handler)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment