page.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. previousState = currentState
  37. currentState = context.state
  38. if res = page.dispatch(context)
  39. currentState = previousState
  40. location.assign(res)
  41. else
  42. context.pushState()
  43. updateCanonicalLink()
  44. track()
  45. context
  46. page.replace = (path, state, skipDispatch, init) ->
  47. context = new Context(path, state or currentState)
  48. context.init = init
  49. currentState = context.state
  50. result = page.dispatch(context) unless skipDispatch
  51. if result
  52. context = new Context(result)
  53. context.init = init
  54. currentState = context.state
  55. page.dispatch(context)
  56. context.replaceState()
  57. updateCanonicalLink()
  58. track() unless skipDispatch
  59. context
  60. page.dispatch = (context) ->
  61. i = 0
  62. next = ->
  63. res = fn(context, next) if fn = callbacks[i++]
  64. return res
  65. return next()
  66. page.canGoBack = ->
  67. not Context.isIntialState(currentState)
  68. page.canGoForward = ->
  69. not Context.isLastState(currentState)
  70. currentPath = ->
  71. location.pathname + location.search + location.hash
  72. class Context
  73. @initialPath: currentPath()
  74. @sessionId: Date.now()
  75. @stateId: 0
  76. @isIntialState: (state) ->
  77. state.id == 0
  78. @isLastState: (state) ->
  79. state.id == @stateId - 1
  80. @isInitialPopState: (state) ->
  81. state.path is @initialPath and @stateId is 1
  82. @isSameSession: (state) ->
  83. state.sessionId is @sessionId
  84. constructor: (@path = '/', @state = {}) ->
  85. @pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) =>
  86. @query = query
  87. @hash = hash
  88. ''
  89. @state.id ?= @constructor.stateId++
  90. @state.sessionId ?= @constructor.sessionId
  91. @state.path = @path
  92. pushState: ->
  93. history.pushState @state, '', @path
  94. return
  95. replaceState: ->
  96. try history.replaceState @state, '', @path # NS_ERROR_FAILURE in Firefox
  97. return
  98. class Route
  99. constructor: (@path, options = {}) ->
  100. @keys = []
  101. @regexp = pathtoRegexp @path, @keys
  102. middleware: (fn) ->
  103. (context, next) =>
  104. if @match context.pathname, params = []
  105. context.params = params
  106. return fn(context, next)
  107. else
  108. return next()
  109. match: (path, params) ->
  110. return unless matchData = @regexp.exec(path)
  111. for value, i in matchData[1..]
  112. value = decodeURIComponent value if typeof value is 'string'
  113. if key = @keys[i]
  114. params[key.name] = value
  115. else
  116. params.push value
  117. true
  118. pathtoRegexp = (path, keys) ->
  119. return path if path instanceof RegExp
  120. path = "(#{path.join '|'})" if path instanceof Array
  121. path = path
  122. .replace /\/\(/g, '(?:/'
  123. .replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) ->
  124. keys.push name: key, optional: !!optional
  125. str = if optional then '' else slash
  126. str += '(?:'
  127. str += slash if optional
  128. str += format
  129. str += capture or if format then '([^/.]+?)' else '([^/]+?)'
  130. str += ')'
  131. str += optional if optional
  132. str
  133. .replace /([\/.])/g, '\\$1'
  134. .replace /\*/g, '(.*)'
  135. new RegExp "^#{path}$"
  136. onpopstate = (event) ->
  137. return if not event.state or Context.isInitialPopState(event.state)
  138. if Context.isSameSession(event.state)
  139. page.replace(event.state.path, event.state)
  140. else
  141. location.reload()
  142. return
  143. onclick = (event) ->
  144. try
  145. return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented
  146. catch
  147. return
  148. link = $.eventTarget(event)
  149. link = link.parentNode while link and link.tagName isnt 'A'
  150. if link and not link.target and isSameOrigin(link.href)
  151. event.preventDefault()
  152. path = link.pathname + link.search + link.hash
  153. path = path.replace /^\/\/+/, '/' # IE11 bug
  154. page.show(path)
  155. return
  156. isSameOrigin = (url) ->
  157. url.indexOf("#{location.protocol}//#{location.hostname}") is 0
  158. updateCanonicalLink = ->
  159. @canonicalLink ||= document.head.querySelector('link[rel="canonical"]')
  160. @canonicalLink.setAttribute('href', "https://#{location.host}#{location.pathname}")
  161. trackers = []
  162. page.track = (fn) ->
  163. trackers.push(fn)
  164. return
  165. track = ->
  166. return unless app.config.env == 'production'
  167. return if navigator.doNotTrack == '1'
  168. return if navigator.globalPrivacyControl
  169. consentGiven = Cookies.get('analyticsConsent')
  170. consentAsked = Cookies.get('analyticsConsentAsked')
  171. if consentGiven == '1'
  172. tracker.call() for tracker in trackers
  173. else if consentGiven == undefined and consentAsked == undefined
  174. # Only ask for consent once per browser session
  175. Cookies.set('analyticsConsentAsked', '1')
  176. new app.views.Notif 'AnalyticsConsent', autoHide: null
  177. return
  178. @resetAnalytics = ->
  179. for cookie in document.cookie.split(/;\s?/)
  180. name = cookie.split('=')[0]
  181. if name[0] == '_' && name[1] != '_'
  182. Cookies.expire(name)
  183. return