page.coffee 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. ###
  2. * Based on github.com/visionmedia/page.js
  3. * Licensed under the MIT license
  4. * Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
  5. ###
  6. running = false
  7. currentState = null
  8. callbacks = []
  9. @page = (value, fn) ->
  10. if typeof value is 'function'
  11. page '*', value
  12. else if typeof fn is 'function'
  13. route = new Route(value)
  14. callbacks.push route.middleware(fn)
  15. else if typeof value is 'string'
  16. page.show(value, fn)
  17. else
  18. page.start(value)
  19. return
  20. page.start = (options = {}) ->
  21. unless running
  22. running = true
  23. addEventListener 'popstate', onpopstate
  24. addEventListener 'click', onclick
  25. page.replace currentPath(), null, null, true
  26. return
  27. page.stop = ->
  28. if running
  29. running = false
  30. removeEventListener 'click', onclick
  31. removeEventListener 'popstate', onpopstate
  32. return
  33. page.show = (path, state) ->
  34. return if path is currentState?.path
  35. context = new Context(path, state)
  36. currentState = context.state
  37. page.dispatch(context)
  38. context.pushState()
  39. track()
  40. context
  41. page.replace = (path, state, skipDispatch, init) ->
  42. context = new Context(path, state or currentState)
  43. context.init = init
  44. currentState = context.state
  45. page.dispatch(context) unless skipDispatch
  46. context.replaceState()
  47. track() unless init or skipDispatch
  48. context
  49. page.dispatch = (context) ->
  50. i = 0
  51. next = ->
  52. fn(context, next) if fn = callbacks[i++]
  53. return
  54. next()
  55. return
  56. page.canGoBack = ->
  57. not Context.isIntialState(currentState)
  58. page.canGoForward = ->
  59. not Context.isLastState(currentState)
  60. currentPath = ->
  61. location.pathname + location.search + location.hash
  62. class Context
  63. @initialPath: currentPath()
  64. @sessionId: Date.now()
  65. @stateId: 0
  66. @isIntialState: (state) ->
  67. state.id == 0
  68. @isLastState: (state) ->
  69. state.id == @stateId - 1
  70. @isInitialPopState: (state) ->
  71. state.path is @initialPath and @stateId is 1
  72. @isSameSession: (state) ->
  73. state.sessionId is @sessionId
  74. constructor: (@path = '/', @state = {}) ->
  75. @pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) =>
  76. @query = query
  77. @hash = hash
  78. ''
  79. @state.id ?= @constructor.stateId++
  80. @state.sessionId ?= @constructor.sessionId
  81. @state.path = @path
  82. pushState: ->
  83. history.pushState @state, '', @path
  84. return
  85. replaceState: ->
  86. try history.replaceState @state, '', @path # NS_ERROR_FAILURE in Firefox
  87. return
  88. class Route
  89. constructor: (@path, options = {}) ->
  90. @keys = []
  91. @regexp = pathtoRegexp @path, @keys
  92. middleware: (fn) ->
  93. (context, next) =>
  94. if @match context.pathname, params = []
  95. context.params = params
  96. fn(context, next)
  97. else
  98. next()
  99. return
  100. match: (path, params) ->
  101. return unless matchData = @regexp.exec(path)
  102. for value, i in matchData[1..]
  103. value = decodeURIComponent value if typeof value is 'string'
  104. if key = @keys[i]
  105. params[key.name] = value
  106. else
  107. params.push value
  108. true
  109. pathtoRegexp = (path, keys) ->
  110. return path if path instanceof RegExp
  111. path = "(#{path.join '|'})" if path instanceof Array
  112. path = path
  113. .replace /\/\(/g, '(?:/'
  114. .replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) ->
  115. keys.push name: key, optional: !!optional
  116. str = if optional then '' else slash
  117. str += '(?:'
  118. str += slash if optional
  119. str += format
  120. str += capture or if format then '([^/.]+?)' else '([^/]+?)'
  121. str += ')'
  122. str += optional if optional
  123. str
  124. .replace /([\/.])/g, '\\$1'
  125. .replace /\*/g, '(.*)'
  126. new RegExp "^#{path}$"
  127. onpopstate = (event) ->
  128. return if not event.state or Context.isInitialPopState(event.state)
  129. if Context.isSameSession(event.state)
  130. page.replace(event.state.path, event.state)
  131. else
  132. location.reload()
  133. return
  134. onclick = (event) ->
  135. try
  136. return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented
  137. catch
  138. return
  139. link = event.target
  140. link = link.parentElement while link and link.tagName isnt 'A'
  141. if link and not link.target and isSameOrigin(link.href)
  142. event.preventDefault()
  143. page.show link.pathname + link.search + link.hash
  144. return
  145. isSameOrigin = (url) ->
  146. url.indexOf("#{location.protocol}//#{location.hostname}") is 0
  147. track = ->
  148. ga?('send', 'pageview', location.pathname + location.search + location.hash)
  149. _gauges?.push(['track'])
  150. return