diff --git a/build/update.sh b/build/update.sh old mode 100644 new mode 100755 index df3887c..42aa3c6 --- a/build/update.sh +++ b/build/update.sh @@ -1,9 +1,9 @@ -TAG=v1.3.0 +DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +ROOT="$(dirname "$DIR")" FILES=( ajax list local manager relation route ) -curl -s -o vendor/assets/javascripts/spine.coffee https://raw.github.com/spine/spine/$TAG/src/spine.coffee +curl -s -o $ROOT/vendor/assets/javascripts/spine.coffee https://raw.githubusercontent.com/spine/spine/dev/src/spine.coffee for i in "${FILES[@]}" do - : - curl -s -o vendor/assets/javascripts/spine/$i.coffee https://raw.github.com/spine/spine/$TAG/src/$i.coffee + curl -s -o $ROOT/vendor/assets/javascripts/spine/$i.coffee https://raw.githubusercontent.com/spine/spine/dev/src/$i.coffee done diff --git a/lib/spine/rails/version.rb b/lib/spine/rails/version.rb index 5187ce6..ea8406f 100644 --- a/lib/spine/rails/version.rb +++ b/lib/spine/rails/version.rb @@ -1,6 +1,6 @@ module Spine module Rails - VERSION = "0.1.3" - SPINE_VERSION = "1.3.0" + VERSION = "0.1.6.1" + SPINE_VERSION = "1.6.1" end end diff --git a/spine-rails.gemspec b/spine-rails.gemspec index 5ef91bb..0a4310c 100644 --- a/spine-rails.gemspec +++ b/spine-rails.gemspec @@ -21,4 +21,4 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split("\n") s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/} s.require_path = 'lib' -end \ No newline at end of file +end diff --git a/vendor/assets/javascripts/spine.coffee b/vendor/assets/javascripts/spine.coffee index 7c898f1..8e59c88 100755 --- a/vendor/assets/javascripts/spine.coffee +++ b/vendor/assets/javascripts/spine.coffee @@ -6,7 +6,7 @@ Released under the MIT License Events = bind: (ev, callback) -> evs = ev.split(' ') - @_callbacks = {} unless @hasOwnProperty('_callbacks') and @_callbacks + @_callbacks or= {} unless @hasOwnProperty('_callbacks') for name in evs @_callbacks[name] or= [] @_callbacks[name].push(callback) @@ -18,12 +18,11 @@ Events = callback.apply(this, arguments) trigger: (args...) -> - ev = args.shift() - list = @hasOwnProperty('_callbacks') and @_callbacks?[ev] + ev = args.shift() + list = @_callbacks?[ev] return unless list for callback in list - if callback.apply(this, args) is false - break + break if callback.apply(this, args) is false true listenTo: (obj, ev, callback) -> @@ -37,39 +36,41 @@ Events = obj.bind ev, handler = -> idx = -1 for lt, i in listeningToOnce when lt.obj is obj - idx = i if lt.ev is ev and lt.callback is callback + idx = i if lt.ev is ev and lt.callback is handler obj.unbind(ev, handler) listeningToOnce.splice(idx, 1) unless idx is -1 callback.apply(this, arguments) - listeningToOnce.push {obj, ev, callback, handler} + listeningToOnce.push {obj, ev, callback: handler} this stopListening: (obj, events, callback) -> if arguments.length is 0 for listeningTo in [@listeningTo, @listeningToOnce] - continue unless listeningTo + continue unless listeningTo?.length for lt in listeningTo - lt.obj.unbind(lt.ev, lt.handler or lt.callback) + lt.obj.unbind(lt.ev, lt.callback) @listeningTo = undefined @listeningToOnce = undefined else if obj + events = if events then events.split(' ') else [undefined] for listeningTo in [@listeningTo, @listeningToOnce] continue unless listeningTo - events = if events then events.split(' ') else [undefined] for ev in events for idx in [listeningTo.length-1..0] lt = listeningTo[idx] - continue if callback and (lt.handler or lt.callback) isnt callback + continue unless lt.obj is obj + continue if callback and lt.callback isnt callback if (not ev) or (ev is lt.ev) - lt.obj.unbind(lt.ev, lt.handler or lt.callback) + lt.obj.unbind(lt.ev, lt.callback) listeningTo.splice(idx, 1) unless idx is -1 else if ev evts = lt.ev.split(' ') if ev in evts evts = (e for e in evts when e isnt ev) lt.ev = $.trim(evts.join(' ')) - lt.obj.unbind(ev, lt.handler or lt.callback) + lt.obj.unbind(ev, lt.callback) + this unbind: (ev, callback) -> if arguments.length is 0 @@ -90,7 +91,7 @@ Events = break this -Events.on = Events.bind +Events.on = Events.bind Events.off = Events.unbind Log = @@ -132,6 +133,7 @@ class Module class Model extends Module @extend Events + @include Events @records : [] @irecords : {} @@ -149,28 +151,31 @@ class Model extends Module @toString: -> "#{@className}(#{@attributes.join(", ")})" @find: (id, notFound = @notFound) -> - record = @irecords[id]?.clone() - return record or notFound?(id) + @irecords[id]?.clone() or notFound?(id) - @notFound: (id) -> return null + @findAll: (ids, notFound) -> + (@find(id) for id in ids when @find(id, notFound)) - @exists: (id) -> - return if @irecords[id] then true else false + @notFound: (id) -> null - @addRecord: (record) -> - if record.id and @irecords[record.id] - @irecords[record.id].remove() + @exists: (id) -> Boolean @irecords[id] - record.id or= record.cid - @records.push(record) - @irecords[record.id] = record - @irecords[record.cid] = record + @addRecord: (record,idx) -> + if root = @irecords[record.id or record.cid] + root.refresh(record) + else + record.id or= record.cid + @irecords[record.id] = @irecords[record.cid] = record + if idx isnt undefined + @records.splice(idx,0,record) + else + @records.push(record) + record @refresh: (values, options = {}) -> @deleteAll() if options.clear - records = @fromJSON(values) - records = [records] unless isArray(records) + records = [records] unless Array.isArray(records) @addRecord(record) for record in records @sort() @@ -230,7 +235,7 @@ class Model extends Module record.save(options) @destroy: (id, options) -> - @find(id).destroy(options) + @find(id)?.destroy(options) @change: (callbackOrParams) -> if typeof callbackOrParams is 'function' @@ -247,13 +252,21 @@ class Model extends Module @toJSON: -> @records + @beforeFromJSON: (objects) -> objects + @fromJSON: (objects) -> return unless objects if typeof objects is 'string' objects = JSON.parse(objects) - if isArray(objects) - (new @(value) for value in objects) + objects = @beforeFromJSON(objects) + if Array.isArray(objects) + for value in objects + if value instanceof this + value + else + new @(value) else + return objects if objects instanceof this new @(objects) @fromForm: -> @@ -282,7 +295,7 @@ class Model extends Module super if @constructor.uuid? and typeof @constructor.uuid is 'function' @cid = @constructor.uuid() - @id = @cid unless @id + @id = @cid unless @id else @cid = atts?.cid or @constructor.uid('c-') @load atts if atts @@ -298,7 +311,8 @@ class Model extends Module load: (atts) -> if atts.id then @id = atts.id for key, value of atts - if atts.hasOwnProperty(key) and typeof @[key] is 'function' + if typeof @[key] is 'function' + continue if typeof value is 'function' @[key](value) else @[key] = value @@ -315,20 +329,20 @@ class Model extends Module result eql: (rec) -> - !!(rec and rec.constructor is @constructor and - ((rec.cid is @cid) or (rec.id and rec.id is @id))) + rec and rec.constructor is @constructor and + ((rec.cid is @cid) or (rec.id and rec.id is @id)) save: (options = {}) -> unless options.validate is false error = @validate() if error - @trigger('error', error) + @trigger('error', this, error) return false - @trigger('beforeSave', options) + @trigger('beforeSave', this, options) record = if @isNew() then @create(options) else @update(options) @stripCloneAttrs() - @trigger('save', options) + @trigger('save', record, options) record stripCloneAttrs: -> @@ -352,28 +366,29 @@ class Model extends Module records[id] = records[@id] delete records[@id] unless @cid is @id @id = id - @save() + #@save() - remove: -> + remove: (options = {}) -> # Remove record from model records = @constructor.records.slice(0) for record, i in records when @eql(record) records.splice(i, 1) break @constructor.records = records - # Remove the ID and CID - delete @constructor.irecords[@id] - delete @constructor.irecords[@cid] + if options.clear + # Remove the ID and CID indexes + delete @constructor.irecords[@id] + delete @constructor.irecords[@cid] destroy: (options = {}) -> - @trigger('beforeDestroy', options) - @remove() + options.clear ?= true + @trigger('beforeDestroy', this, options) + @remove(options) @destroyed = true # handle events - @trigger('destroy', options) - @trigger('change', 'destroy', options) - if @listeningTo - @stopListening() + @trigger('destroy', this, options) + @trigger('change', this, 'destroy', options) + @stopListening() if @listeningTo @unbind() this @@ -383,7 +398,9 @@ class Model extends Module delete atts.id else atts.cid = @cid - new @constructor(atts) + record = new @constructor(atts) + @_callbacks and record._callbacks = @_callbacks unless newRecord + record clone: -> createObject(this) @@ -394,12 +411,16 @@ class Model extends Module @load(original.attributes()) original - refresh: (data) -> + refresh: (atts) -> + atts = @constructor.fromJSON(atts) + # ID change, need to do some shifting + if atts.id and @id isnt atts.id + @changeID(atts.id) # go to the source and load attributes - root = @constructor.irecords[@id] - root.load(data) - @trigger('refresh') - @ + @constructor.irecords[@id].load(atts) + @trigger('refresh', this) + @trigger('change', this, 'refresh') + this toJSON: -> @attributes() @@ -429,7 +450,7 @@ class Model extends Module # Private update: (options) -> - @trigger('beforeUpdate', options) + @trigger('beforeUpdate', this, options) records = @constructor.irecords records[@id].load @attributes() @@ -437,61 +458,46 @@ class Model extends Module @constructor.sort() clone = records[@id].clone() - clone.trigger('update', options) - clone.trigger('change', 'update', options) + clone.trigger('update', clone, options) + clone.trigger('change', clone, 'update', options) clone create: (options) -> - @trigger('beforeCreate', options) + @trigger('beforeCreate', this, options) @id or= @cid record = @dup(false) - @constructor.addRecord(record) + @constructor.addRecord(record,options.idx) @constructor.sort() - clone = record.clone() - clone.trigger('create', options) - clone.trigger('change', 'create', options) + clone = record.clone() + clone.trigger('create', clone, options) + clone.trigger('change', clone, 'create', options) clone - bind: (events, callback) -> - @constructor.bind events, binder = (record) => - if record && @eql(record) - callback.apply(this, arguments) - # create a wrapper function to be called with 'unbind' for each event - for singleEvent in events.split(' ') - do (singleEvent) => - @constructor.bind "unbind", unbinder = (record, event, cb) => - if record && @eql(record) - return if event and event isnt singleEvent - return if cb and cb isnt callback - @constructor.unbind(singleEvent, binder) - @constructor.unbind("unbind", unbinder) - this - - one: (events, callback) -> - @bind events, handler = => - @unbind(events, handler) - callback.apply(this, arguments) + bind: -> + record = @constructor.irecords[@id] or this + Events.bind.apply record, arguments - trigger: (args...) -> - args.splice(1, 0, this) - @constructor.trigger(args...) + one: -> + record = @constructor.irecords[@id] or this + Events.one.apply record, arguments - listenTo: -> Events.listenTo.apply @, arguments - listenToOnce: -> Events.listenToOnce.apply @, arguments - stopListening: -> Events.stopListening.apply @, arguments + unbind: -> + record = @constructor.irecords[@id] or this + Events.unbind.apply record, arguments - unbind: (events, callback) -> - if arguments.length is 0 - @trigger('unbind') - else if events - for event in events.split(' ') - @trigger('unbind', event, callback) + trigger: -> + Events.trigger.apply this, arguments # Trigger the instance event. + # Don't trigger 'refresh' multiple times on the class - the class method + # will trigger it once for the whole refresh operation. + return true if arguments[0] is 'refresh' + @constructor.trigger arguments... # Trigger the class event. -Model::on = Model::bind +Model::on = Model::bind Model::off = Model::unbind + class Controller extends Module @include Events @include Log @@ -505,9 +511,8 @@ class Controller extends Module for key, value of @options @[key] = value - @el = document.createElement(@tag) unless @el - @el = $(@el) - @$el = @el + @el = document.createElement(@tag) unless @el + @el = $(@el) @el.addClass(@className) if @className @el.attr(@attributes) if @attributes @@ -533,7 +538,7 @@ class Controller extends Module @unbind() @stopListening() - $: (selector) -> $(selector, @el) + $: (selector) -> @el.find(selector) delegateEvents: (events) -> for key, method of events @@ -610,14 +615,6 @@ createObject = Object.create or (o) -> Func.prototype = o new Func() -isArray = (value) -> - Object::toString.call(value) is '[object Array]' - -isBlank = (value) -> - return true unless value - return false for key of value - true - makeArray = (args) -> Array::slice.call(args, 0) @@ -626,9 +623,7 @@ makeArray = (args) -> Spine = @Spine = {} module?.exports = Spine -Spine.version = '1.3.0' -Spine.isArray = isArray -Spine.isBlank = isBlank +Spine.version = '1.6.1' Spine.$ = $ Spine.Events = Events Spine.Log = Log diff --git a/vendor/assets/javascripts/spine/ajax.coffee b/vendor/assets/javascripts/spine/ajax.coffee index 2007f0a..05b664c 100755 --- a/vendor/assets/javascripts/spine/ajax.coffee +++ b/vendor/assets/javascripts/spine/ajax.coffee @@ -9,13 +9,13 @@ Ajax = @generateURL(object) else @generateURL(object, encodeURIComponent(object.id)) - + getCollectionURL: (object) -> @generateURL(object) - + getScope: (object) -> object.scope?() or object.scope - + getCollection: (object) -> if object.url isnt object.generateURL if typeof object.url is 'function' @@ -60,6 +60,12 @@ Ajax = clearQueue: -> @queue [] + config: + loadMethod: 'GET' + updateMethod: 'PUT' + createMethod: 'POST' + destroyMethod: 'DELETE' + class Base defaults: dataType: 'json' @@ -88,10 +94,15 @@ class Base # 2 reasons not to stringify: if already a string, or if intend to have ajax processData if typeof settings.data isnt 'string' and settings.processData isnt true settings.data = JSON.stringify(settings.data) + # enable promise callbacks to access the request's settings object + resolve = -> + deferred.resolve.apply this, [arguments..., settings] + reject = -> + deferred.reject.apply this, [arguments..., settings] jqXHR = $.ajax(settings) - .done(deferred.resolve) - .fail(deferred.reject) - .then(next, next) + jqXHR.done(resolve) + jqXHR.fail(reject) + jqXHR.then(next, next) if parallel Queue.dequeue() @@ -104,7 +115,7 @@ class Base [promise, statusText, ''] ) promise - + @queue request promise @@ -118,22 +129,22 @@ class Collection extends Base record = new @model(id: id) @ajaxQueue( params, { - type: 'GET' + type: options.method or Ajax.config.loadMethod url: options.url or Ajax.getURL(record) parallel: options.parallel } - ).done(@recordsResponse) - .fail(@failResponse) + ).done(@recordsResponse(options)) + .fail(@failResponse(options)) all: (params, options = {}) -> @ajaxQueue( params, { - type: 'GET' + type: options.method or Ajax.config.loadMethod url: options.url or Ajax.getURL(@model) parallel: options.parallel } - ).done(@recordsResponse) - .fail(@failResponse) + ).done(@recordsResponse(options)) + .fail(@failResponse(options)) fetch: (params = {}, options = {}) -> if id = params.id @@ -146,11 +157,15 @@ class Collection extends Base # Private - recordsResponse: (data, status, xhr) => - @model.trigger('ajaxSuccess', null, status, xhr) + recordsResponse: (options) => + (data, status, xhr, settings) => + @model.trigger('ajaxSuccess', null, status, xhr, settings) + options.done?.call(@model, settings) - failResponse: (xhr, statusText, error) => - @model.trigger('ajaxError', null, xhr, statusText, error) + failResponse: (options) => + (xhr, statusText, error, settings) => + @model.trigger('ajaxError', null, xhr, statusText, error, settings) + options.fail?.call(@model, settings) class Singleton extends Base constructor: (@record) -> @@ -159,67 +174,70 @@ class Singleton extends Base reload: (params, options = {}) -> @ajaxQueue( params, { - type: 'GET' + type: options.method or Ajax.config.loadMethod url: options.url parallel: options.parallel }, @record ).done(@recordResponse(options)) - .fail(@failResponse(options)) + .fail(@failResponse(options)) create: (params, options = {}) -> @ajaxQueue( params, { - type: 'POST' + type: options.method or Ajax.config.createMethod contentType: 'application/json' data: @record.toJSON() url: options.url or Ajax.getCollectionURL(@record) parallel: options.parallel } ).done(@recordResponse(options)) - .fail(@failResponse(options)) + .fail(@failResponse(options)) update: (params, options = {}) -> @ajaxQueue( params, { - type: 'PUT' + type: options.method or Ajax.config.updateMethod contentType: 'application/json' data: @record.toJSON() url: options.url parallel: options.parallel }, @record ).done(@recordResponse(options)) - .fail(@failResponse(options)) + .fail(@failResponse(options)) destroy: (params, options = {}) -> @ajaxQueue( params, { - type: 'DELETE' + type: options.method or Ajax.config.destroyMethod url: options.url parallel: options.parallel }, @record ).done(@recordResponse(options)) - .fail(@failResponse(options)) + .fail(@failResponse(options)) # Private - recordResponse: (options = {}) => - (data, status, xhr) => + recordResponse: (options) => + (data, status, xhr, settings) => + if data? and Object.getOwnPropertyNames(data).length and not @record.destroyed + @record.refresh(data, ajax: false) + @record.trigger('ajaxSuccess', @record, @model.fromJSON(data), status, xhr, settings) + options.done?.call(@record, settings) - Ajax.disable => - unless Spine.isBlank(data) or @record.destroyed - # ID change, need to do some shifting - if data.id and @record.id isnt data.id - @record.changeID(data.id) - # Update with latest data - @record.refresh(data) + failResponse: (options) => + (xhr, statusText, error, settings) => + switch settings.type + when 'POST' then @createFailed() + when 'DELETE' then @destroyFailed() + @record.trigger('ajaxError', @record, xhr, statusText, error, settings) + options.fail?.call(@record, settings) - @record.trigger('ajaxSuccess', data, status, xhr) - options.done?.apply(@record) + createFailed: -> + @record.remove(clear: true) - failResponse: (options = {}) => - (xhr, statusText, error) => - @record.trigger('ajaxError', xhr, statusText, error) - options.fail?.apply(@record) + destroyFailed: -> + @record.destroyed = false + @record.constructor.refresh(@record) # Ajax endpoint Model.host = '' @@ -227,22 +245,22 @@ Model.host = '' GenerateURL = include: (args...) -> args.unshift(encodeURIComponent(@id)) - Ajax.generateURL(@, args...) + Ajax.generateURL(this, args...) extend: (args...) -> - Ajax.generateURL(@, args...) + Ajax.generateURL(this, args...) Include = ajax: -> new Singleton(this) - + generateURL: GenerateURL.include - + url: GenerateURL.include - + Extend = ajax: -> new Collection(this) - + generateURL: GenerateURL.extend - + url: GenerateURL.extend Model.Ajax = @@ -259,7 +277,7 @@ Model.Ajax = ajaxChange: (record, type, options = {}) -> return if options.ajax is false - record.ajax()[type](options.ajax, options) + record.ajax()[type]?(options.ajax, options) Model.Ajax.Methods = extended: -> diff --git a/vendor/assets/javascripts/spine/local.coffee b/vendor/assets/javascripts/spine/local.coffee index ff3b688..63d2418 100755 --- a/vendor/assets/javascripts/spine/local.coffee +++ b/vendor/assets/javascripts/spine/local.coffee @@ -2,6 +2,13 @@ Spine = @Spine or require('spine') Spine.Model.Local = extended: -> + testLocalStorage = 'spine' + new Date().getTime() + try + localStorage.setItem(testLocalStorage, testLocalStorage) + localStorage.removeItem(testLocalStorage) + catch e + return + @change @saveLocal @fetch @loadLocal @@ -14,4 +21,4 @@ Spine.Model.Local = result = localStorage[@className] @refresh(result or [], options) -module?.exports = Spine.Model.Local \ No newline at end of file +module?.exports = Spine.Model.Local diff --git a/vendor/assets/javascripts/spine/manager.coffee b/vendor/assets/javascripts/spine/manager.coffee index 3d44e39..4206f40 100755 --- a/vendor/assets/javascripts/spine/manager.coffee +++ b/vendor/assets/javascripts/spine/manager.coffee @@ -1,5 +1,6 @@ -Spine = @Spine or require('spine') -$ = Spine.$ +Spine = @Spine or require('spine') +$ = Spine.$ + class Spine.Manager extends Spine.Module @include Spine.Events @@ -31,6 +32,7 @@ class Spine.Manager extends Spine.Module current.activate(args...) if current + Spine.Controller.include active: (args...) -> if typeof args[0] is 'function' @@ -45,11 +47,12 @@ Spine.Controller.include activate: -> @el.addClass('active') - @ + this deactivate: -> @el.removeClass('active') - @ + this + class Spine.Stack extends Spine.Controller controllers: {} @@ -61,10 +64,11 @@ class Spine.Stack extends Spine.Controller super @manager = new Spine.Manager + @router = Spine.Route?.create() for key, value of @controllers - throw Error "'@#{ key }' already assigned - choose a different name" if @[key]? - @[key] = new value(stack: @) + throw Error "'@#{ key }' already assigned" if @[key]? + @[key] = new value(stack: this) @add(@[key]) for key, value of @routes @@ -79,5 +83,10 @@ class Spine.Stack extends Spine.Controller @manager.add(controller) @append(controller) -module?.exports = Spine.Manager + release: => + @router?.destroy() + super + + +module?.exports = Spine.Manager module?.exports.Stack = Spine.Stack diff --git a/vendor/assets/javascripts/spine/relation.coffee b/vendor/assets/javascripts/spine/relation.coffee index 2c50670..99056cf 100755 --- a/vendor/assets/javascripts/spine/relation.coffee +++ b/vendor/assets/javascripts/spine/relation.coffee @@ -1,6 +1,4 @@ Spine = @Spine or require('spine') -isArray = Spine.isArray -require = @require or ((value) -> eval(value)) class Collection extends Spine.Module constructor: (options = {}) -> @@ -43,7 +41,7 @@ class Collection extends Spine.Module for match, i in @model.records when match.id is record.id @model.records.splice(i, 1) break - values = [values] unless isArray(values) + values = [values] unless Array.isArray(values) for record in values record.newRecord = false record[@fkey] = @record.id @@ -70,11 +68,8 @@ class Instance extends Spine.Module for key, value of options @[key] = value - exists: -> - return if @record[@fkey] then @model.exists(@record[@fkey]) else false - find: -> - return @model.find(@record[@fkey]) + @model.find(@record[@fkey]) update: (value) -> return this unless value? @@ -108,11 +103,17 @@ underscore = (str) -> str.replace(/::/g, '/') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') .replace(/([a-z\d])([A-Z])/g, '$1_$2') - .replace(/-/g, '_') + .replace(/(-|\.)/g, '_') .toLowerCase() +requireModel = (model) -> + if typeof model is 'string' + require?(model) or eval(model) + else + model + association = (name, model, record, fkey, Ctor) -> - model = require(model) if typeof model is 'string' + model = requireModel(model) if typeof model is 'string' new Ctor(name: name, model: model, record: record, fkey: fkey) Spine.Model.extend diff --git a/vendor/assets/javascripts/spine/route.coffee b/vendor/assets/javascripts/spine/route.coffee index 0ecb632..1c98ffa 100755 --- a/vendor/assets/javascripts/spine/route.coffee +++ b/vendor/assets/javascripts/spine/route.coffee @@ -6,13 +6,48 @@ namedParam = /:([\w\d]+)/g splatParam = /\*([\w\d]+)/g escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g -class Spine.Route extends Spine.Module + +class Path extends Spine.Module + + constructor: (path, callback) -> + @names = [] + @path = path + @callback = callback + if typeof path is 'string' + namedParam.lastIndex = 0 + while (match = namedParam.exec(path)) != null + @names.push(match[1]) + + splatParam.lastIndex = 0 + while (match = splatParam.exec(path)) != null + @names.push(match[1]) + + path = path.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]*)') + .replace(splatParam, '(.*?)') + + @route = new RegExp("^#{path}$") + else + @route = path + + match: (path, options = {}) -> + return false unless match = @route.exec(path) + options.match = match + params = match.slice(1) + + if @names.length + for param, i in params + options[@names[i]] = param + + Route.trigger('before', this) + @callback.call(null, options) isnt false + + +class Route extends Spine.Module @extend Spine.Events @historySupport: window.history?.pushState? - @routes: [] - @options: trigger: true history: false @@ -20,16 +55,12 @@ class Spine.Route extends Spine.Module replace: false redirect: false - @add: (path, callback) -> - if (typeof path is 'object' and path not instanceof RegExp) - @add(key, value) for key, value of path - else - @routes.push(new @(path, callback)) + @routers: [] @setup: (options = {}) -> @options = $.extend({}, @options, options) - if (@options.history) + if @options.history @history = @historySupport and @options.history return if @options.shim @@ -41,6 +72,9 @@ class Spine.Route extends Spine.Module @change() @unbind: -> + unbindResult = Spine.Events.unbind.apply this, arguments + return unbindResult if arguments.length > 0 + return if @options.shim if @history @@ -50,39 +84,55 @@ class Spine.Route extends Spine.Module @navigate: (args...) -> options = {} - lastArg = args[args.length - 1] if typeof lastArg is 'object' options = args.pop() else if typeof lastArg is 'boolean' options.trigger = args.pop() - options = $.extend({}, @options, options) path = args.join('/') return if @path is path @path = path - @trigger('navigate', @path) - - route = @matchRoute(@path, options) if options.trigger - - return if options.shim - - if !route - if typeof options.redirect is 'function' - return options.redirect.apply this, [@path, options] - else - if options.redirect is true - @redirect(@path) - - if @history and options.replace + if options.trigger + @trigger('navigate', @path) + routes = @matchRoutes(@path, options) + unless routes.length + if typeof options.redirect is 'function' + return options.redirect.apply this, [@path, options] + else + if options.redirect is true + @redirect(@path) + + if options.shim + true + else if @history and options.replace history.replaceState({}, document.title, @path) else if @history history.pushState({}, document.title, @path) else window.location.hash = @path + @create: -> + router = new this + @routers.push router + router + + @add: (path, callback) -> + #@router ?= new this + @router.add path, callback + + add: (path, callback) -> + if typeof path is 'object' and path not instanceof RegExp + @add(key, value) for key, value of path + else + @routes.push(new Path(path, callback)) + + destroy: -> + @routes.length = 0 + @constructor.routers = (r for r in @constructor.routers when r isnt this) + # Private @getPath: -> @@ -97,58 +147,43 @@ class Spine.Route extends Spine.Module @getHost: -> "#{window.location.protocol}//#{window.location.host}" - @change: -> + @change: => path = @getPath() return if path is @path @path = path - @matchRoute(@path) + @matchRoutes(@path) - @matchRoute: (path, options) -> - for route in @routes when route.match(path, options) - @trigger('change', route, path) - return route + @matchRoutes: (path, options)-> + matches = [] + for router in @routers.concat [@router] + match = router.matchRoute path, options + matches.push match if match + @trigger('change', matches, path) if matches.length + matches @redirect: (path) -> window.location = path - constructor: (@path, @callback) -> - @names = [] - - if typeof path is 'string' - namedParam.lastIndex = 0 - while (match = namedParam.exec(path)) != null - @names.push(match[1]) + constructor: -> + @routes = [] - splatParam.lastIndex = 0 - while (match = splatParam.exec(path)) != null - @names.push(match[1]) - - path = path.replace(escapeRegExp, '\\$&') - .replace(namedParam, '([^\/]*)') - .replace(splatParam, '(.*?)') - - @route = new RegExp("^#{path}$") - else - @route = path + matchRoute: (path, options) -> + for route in @routes when route.match(path, options) + return route - match: (path, options = {}) -> - match = @route.exec(path) - return false unless match - options.match = match - params = match.slice(1) + trigger: (args...) -> + args.splice(1, 0, this) + @constructor.trigger(args...) - if @names.length - for param, i in params - options[@names[i]] = param - @.constructor.trigger('before', @) - @callback.call(null, options) isnt false +Route.router = new Route -# Coffee-script bug -Spine.Route.change = Spine.Route.proxy(Spine.Route.change) Spine.Controller.include route: (path, callback) -> - Spine.Route.add(path, @proxy(callback)) + if @router instanceof Spine.Route + @router.add(path, @proxy(callback)) + else + Spine.Route.add(path, @proxy(callback)) routes: (routes) -> @route(key, value) for key, value of routes @@ -156,4 +191,6 @@ Spine.Controller.include navigate: -> Spine.Route.navigate.apply(Spine.Route, arguments) -module?.exports = Spine.Route +Route.Path = Path +Spine.Route = Route +module?.exports = Route