Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Radically simplify app by bundling chaplin views.

  • Loading branch information...
commit e6eaf5d05c9693567c7ded7c70bdbf8995cb10a3 1 parent 3e120e9
@paulmillr paulmillr authored
Showing with 4,265 additions and 3,420 deletions.
  1. +31 −24 app/application.coffee
  2. +1 −5 app/assets/index.html
  3. +0 −16 app/controllers/application_controller.coffee
  4. +2 −43 app/controllers/controller.coffee
  5. +1 −1  app/controllers/navigation_controller.coffee
  6. +2 −2 app/controllers/session_controller.coffee
  7. +2 −2 app/controllers/sidebar_controller.coffee
  8. +1 −1  app/controllers/tweets_controller.coffee
  9. +6 −0 app/initialize.coffee
  10. +0 −91 app/lib/route.coffee
  11. +0 −58 app/lib/router.coffee
  12. +19 −1 app/lib/services/service_provider.coffee
  13. +0 −37 app/lib/subscriber.coffee
  14. +12 −0 app/lib/support.coffee
  15. +6 −415 app/lib/utils.coffee
  16. +2 −53 app/lib/view_helper.coffee
  17. +27 −36 app/mediator.coffee
  18. +2 −90 app/models/collection.coffee
  19. +2 −39 app/models/model.coffee
  20. +1 −1  app/models/navigation.coffee
  21. +1 −1  app/models/status.coffee
  22. +1 −1  app/models/tweet.coffee
  23. +2 −2 app/models/tweets.coffee
  24. +2 −2 app/models/user.coffee
  25. +4 −0 app/routes.coffee
  26. +0 −320 app/views/application_view.coffee
  27. +0 −368 app/views/collection_view.coffee
  28. +2 −2 app/views/login_view.coffee
  29. +2 −2 app/views/navigation_view.coffee
  30. +1 −4 app/views/sidebar_view.coffee
  31. +2 −2 app/views/stats_view.coffee
  32. +2 −2 app/views/status_view.coffee
  33. +2 −3 app/views/tweet_view.coffee
  34. +9 −3 app/views/tweets_view.coffee
  35. +2 −294 app/views/view.coffee
  36. +1 −5 public/index.html
  37. +127 −1,493 public/javascripts/app.js
  38. +1,995 −1 public/javascripts/vendor.js
  39. +1,993 −0 vendor/scripts/chaplin-610a5cc5.js
View
55 app/application.coffee
@@ -1,34 +1,41 @@
mediator = require 'mediator'
+Application = require 'chaplin/application'
SessionController = require 'controllers/session_controller'
-ApplicationController = require 'controllers/application_controller'
-Router = require 'lib/router'
+NavigationController = require 'controllers/navigation_controller'
+SidebarController = require 'controllers/sidebar_controller'
+routes = require 'routes'
+support = require 'chaplin/lib/support'
# The application bootstrapper.
-# In practise you might choose a more meaningful name.
-Application =
+module.exports = class TwitterApplication extends Application
+ title: 'Tweet your brunch'
+
initialize: ->
- @initControllers()
- @initRouter()
- return
-
- # Instantiate meta-controllers
- initControllers: ->
- # At the moment, do not save the references.
- # They might be safed as instance properties or directly on the mediator.
- # Normally, controllers can communicate with each other via Pub/Sub.
+ #console.debug 'ExampleApplication#initialize'
+
+ super # This creates the AppController and AppView
+
+ # Instantiate common controllers
+ # ------------------------------
+
new SessionController()
- new ApplicationController()
+ new NavigationController()
+ new SidebarController()
+
+ # Initialize the router
+ # ---------------------
- # Instantiate the router
- initRouter: ->
- # We have to make the router public because
- # the AppView needs to access it synchronously.
- mediator.router = new Router()
+ # This creates the mediator.router property and
+ # starts the Backbone history.
+ @initRouter routes
- # Make router property readonly
- Object.defineProperty? mediator, 'router', writable: false
+ # Object sealing
+ # --------------
-# Freeze the object
-Object.freeze? Application
+ # Seal the mediator object (prevent extensions and
+ # make all properties non-configurable)
+ if support.propertyDescriptors and Object.seal
+ Object.seal mediator
-module.exports = Application
+ # Freeze the application instance to prevent further changes
+ Object.freeze? this
View
6 app/assets/index.html
@@ -11,11 +11,7 @@
<link rel="stylesheet" href="stylesheets/app.css">
<script src="javascripts/vendor.js"></script>
<script src="javascripts/app.js"></script>
- <script>
- $(function() {
- require('application').initialize();
- });
- </script>
+ <script>require('initialize');</script>
</head>
<body>
<nav class="navbar navbar-fixed-top" id="navigation-container">
View
16 app/controllers/application_controller.coffee
@@ -1,16 +0,0 @@
-Controller = require 'controllers/controller'
-ApplicationView = require 'views/application_view'
-NavigationController = require 'controllers/navigation_controller'
-SidebarController = require 'controllers/sidebar_controller'
-
-module.exports = class ApplicationController extends Controller
- initialize: ->
- @initApplicationView()
- @initSidebars()
-
- initApplicationView: ->
- new ApplicationView()
-
- initSidebars: ->
- new NavigationController()
- new SidebarController()
View
45 app/controllers/controller.coffee
@@ -1,44 +1,3 @@
-Subscriber = require 'lib/subscriber'
+ChaplinController = require 'chaplin/controllers/controller'
-module.exports = class Controller
- # Mixin a Subscriber
- _(Controller.prototype).defaults Subscriber
-
- model: null
- collection: null
- view: null
- currentId: null
-
- constructor: ->
- @initialize()
-
- initialize: ->
-
- #
- # Disposal
- #
-
- disposed: false
-
- dispose: =>
- return if @disposed
- #console.debug 'Controller#dispose', this
-
- # Dispose models, collections and views
- @model.dispose() if @model # Also disposes associated views
- @collection.dispose() if @collection # Also disposes associated views
- @view.dispose() if @view # Just in case it wasn't disposed indirectly
-
- # Unbind handlers of global events
- @unsubscribeAllEvents()
-
- # Remove model, collection and view references
- properties = 'model collection view currentId'.split(' ')
- delete @[prop] for prop in properties
-
- # Finished
- #console.debug 'Controller#dispose', this, 'finished'
- @disposed = true
-
- # You're frozen when your heart’s not open
- Object.freeze? this
+module.exports = class Controller extends ChaplinController
View
2  app/controllers/navigation_controller.coffee
@@ -1,4 +1,4 @@
-Controller = require 'controllers/controller'
+Controller = require './controller'
mediator = require 'mediator'
Navigation = require 'models/navigation'
NavigationView = require 'views/navigation_view'
View
4 app/controllers/session_controller.coffee
@@ -1,7 +1,7 @@
mediator = require 'mediator'
utils = require 'lib/utils'
User = require 'models/user'
-Controller = require 'controllers/controller'
+Controller = require './controller'
Twitter = require 'lib/services/twitter'
LoginView = require 'views/login_view'
@@ -53,7 +53,7 @@ module.exports = class SessionController extends Controller
createUser: (userData) ->
#console.debug 'SessinController#createUser', userData
user = new User userData
- mediator.user = user
+ mediator.setUser user
# Try to get an existing session from one of the login providers
getSession: ->
View
4 app/controllers/sidebar_controller.coffee
@@ -1,7 +1,7 @@
-Controller = require 'controllers/controller'
+Controller = require './controller'
SidebarView = require 'views/sidebar_view'
StatusView = require 'views/status_view'
-module.exports = class NavigationController extends Controller
+module.exports = class SidebarController extends Controller
initialize: ->
@view = new SidebarView()
View
2  app/controllers/tweets_controller.coffee
@@ -1,4 +1,4 @@
-Controller = require 'controllers/controller'
+Controller = require './controller'
Tweets = require 'models/tweets'
TweetsView = require 'views/tweets_view'
View
6 app/initialize.coffee
@@ -0,0 +1,6 @@
+Application = require './application'
+
+$ ->
+ app = new Application()
+ app.initialize()
+
View
91 app/lib/route.coffee
@@ -1,91 +0,0 @@
-mediator = require 'mediator'
-
-module.exports = class Route
- @reservedParams: 'path changeURL'.split(' ')
-
- constructor: (pattern, target, @options = {}) ->
- #console.debug 'Router#constructor'
-
- # Save the raw pattern
- @pattern = pattern
-
- # Separate target into controller and controller action
- [@controller, @action] = target.split('#')
-
- # Replace :parameters, collecting their names
- @paramNames = []
- pattern = pattern.replace /:(\w+)/g, @addParamName
-
- # Create the actual regular expression
- @regExp = ///^#{pattern}(?=\?|$)/// # End or begin of query string
-
- addParamName: (match, paramName) =>
- # Test if parameter name is reserved
- if _(Route.reservedParams).include(paramName)
- throw new Error "Route#new: parameter name #{paramName} is reserved"
- # Save parameter name
- @paramNames.push paramName
- # Replace with a character class
- '([\\w-]+)'
-
- # Test if the route matches to a path (called by Backbone.History#loadUrl)
- test: (path) ->
- #console.debug 'Route#test', this, "path »#{path}«", typeof path
-
- # Test the main RegExp
- matched = @regExp.test path
- return false unless matched
-
- # Apply the parameter constraints
- constraints = @options.constraints
- if constraints
- params = @extractParams path
- for own name, constraint of constraints
- unless constraint.test(params[name])
- return false
-
- return true
-
- # The handler which is called by Backbone.History when the route matched.
- # It is also called by Router#follow which might pass options
- handler: (path, options) =>
- #console.debug 'Route#handler', this, path, options
-
- # Build params hash
- params = @buildParams path, options
-
- # Publish a global routeMatch event passing the route and the params
- mediator.publish 'matchRoute', this, params
-
- # Create a proper Rails-like params hash, not an array like Backbone
- # `matches` and `additionalParams` arguments are optional
- buildParams: (path, options) ->
- #console.debug 'Route#buildParams', path, options
-
- params = @extractParams path
-
- # Add additional params from options
- # (they might overwrite params extracted from URL)
- _(params).extend @options.params
-
- # Add a param whether to change the URL
- # Defaults to false unless explicitly set in options
- params.changeURL = Boolean(options and options.changeURL)
-
- # Add a param with the whole path match
- params.path = path
- params
-
- # Extract parameters from the URL
- extractParams: (path) ->
- params = {}
-
- # Apply the regular expression
- matches = @regExp.exec path
-
- # Fill the hash using the paramNames and the matches
- for match, index in matches.slice(1)
- paramName = @paramNames[index]
- params[paramName] = match
-
- params
View
58 app/lib/router.coffee
@@ -1,58 +0,0 @@
-mediator = require 'mediator'
-Route = require 'lib/route'
-
-# This class does not inherit from Backbone’s router
-module.exports = class Router
- constructor: ->
- @registerRoutes()
- @startHistory()
-
- registerRoutes: ->
-
- # ---- THE INTREDASTING PART STARTS: ---- #
-
- @match '', 'tweets#index'
- # @match 'mentions', 'tweets#mentions'
- @match '@:user', 'user#show'
- @match 'logout', 'navigation#logout'
- # @match 'mentions', 'mentions#index'
- # @match 'messages', 'messages#index'
-
- # ---- THE INTREDASTING PART ENDS. ---- #
-
- # Start the Backbone History to start routing
- startHistory: ->
- Backbone.history.start pushState: no
-
- # Connect an address with a controller action
- # Directly create a Backbone.history route
- match: (pattern, target, options = {}) ->
- #console.debug 'Router#match', pattern, target
-
- # Create a Backbone history instance (singleton)
- Backbone.history or= new Backbone.History
-
- # Create a route
- route = new Route pattern, target, options
- #console.debug 'created route', route
-
- # Register the route at the Backbone History instance
- Backbone.history.route route, route.handler
-
- # Route a given URL path manually, return whether a route matched
- route: (path) =>
- #console.debug 'Router#route', path
- # Remove leading hash or slash
- path = path.replace /^(\/#|\/)/, ''
- for handler in Backbone.history.handlers
- if handler.route.test(path)
- handler.callback path, changeURL: true
- return true
- false
-
- # Change the current URL, add a history entry.
- # Do not trigger any routes (which is Backbone’s
- # default behavior, but added for clarity)
- changeURL: (url) ->
- #console.debug 'Router#navigate', url
- Backbone.history.navigate url, trigger: false
View
20 app/lib/services/service_provider.coffee
@@ -1,5 +1,5 @@
utils = require 'lib/utils'
-Subscriber = require 'lib/subscriber'
+Subscriber = require 'chaplin/lib/subscriber'
module.exports = class ServiceProvider
# Mixin a Subscriber
@@ -18,6 +18,24 @@ module.exports = class ServiceProvider
methods: ['triggerLogin', 'getLoginStatus']
onDeferral: @loadSDK
+ # Disposal
+ # --------
+
+ disposed: false
+
+ dispose: ->
+ return if @disposed
+
+ # Unbind handlers of global events
+ @unsubscribeAllEvents()
+
+ # Finished
+ #console.debug 'ServiceProvider#dispose', this, 'finished'
+ @disposed = true
+
+ # You're frozen when your heart’s not open
+ Object.freeze? this
+
###
Standard methods and their signatures:
View
37 app/lib/subscriber.coffee
@@ -1,37 +0,0 @@
-mediator = require 'mediator'
-
-# Add functionality to subscribe to global Publish/Subscribe events
-# so they can be removed afterwards when disposing the object
-module.exports = Subscriber =
- # The subscriptions storage
- globalSubscriptions: null
-
- subscribeEvent: (type, handler) ->
- @globalSubscriptions or= {}
- # Add to store
- handlers = @globalSubscriptions[type] or= []
- return if _(handlers).include handler
- handlers.push handler
- # Register global handler
- mediator.subscribe type, handler, this
-
- unsubscribeEvent: (type, handler) ->
- return unless @globalSubscriptions
- # Remove from store
- handlers = @globalSubscriptions[type]
- if handlers
- index = _(handlers).indexOf handler
- handlers.splice index, 1 if index > -1
- delete @globalSubscriptions[type] if handlers.length is 0
- # Remove global handler
- mediator.unsubscribe type, handler
-
- # Unbind all recorded global handlers
- unsubscribeAllEvents: () ->
- return unless @globalSubscriptions
- for own type, handlers of @globalSubscriptions
- for handler in handlers
- # Remove global handler
- mediator.unsubscribe type, handler
- # Clear store
- @globalSubscriptions = null
View
12 app/lib/support.coffee
@@ -0,0 +1,12 @@
+utils = require 'lib/utils'
+chaplinSupport = require 'chaplin/lib/support'
+
+# Application-specific feature detection
+# --------------------------------------
+
+# Delegate to Chaplin’s support module
+module.exports = support = utils.beget chaplinSupport
+
+# _(support).extend
+
+ # someMethod: ->
View
421 app/lib/utils.coffee
@@ -1,36 +1,13 @@
mediator = require 'mediator'
+chaplinUtils = require 'chaplin/lib/utils'
-module.exports = utils =
- # Object Helpers
- # --------------
+# Application-specific utilities
+# ------------------------------
- beget: (obj) ->
- ctor = ->
- ctor:: = obj
- new ctor
+# Delegate to Chaplin’s utils module
+module.exports = utils = chaplinUtils.beget chaplinUtils
- # String Helpers
- # --------------
-
- # camel-case-helper > camelCaseHelper
- camelize: do ->
- regexp = /[-_]([a-z])/g
- camelizer = (match, c) ->
- c.toUpperCase()
- (string) ->
- string.replace regexp, camelizer
-
- # Upcase the first character
- upcase: (str) ->
- str.charAt(0).toUpperCase() + str.substring(1)
-
- # underScoreHelper -> under_score_helper
- underscorize: do ->
- regexp = /[A-Z]/g
- underscorizer = (c) ->
- '_' + c.toLowerCase()
- (string) ->
- string.replace regexp, underscorizer
+_(utils).extend
# Facebook image helper
# ---------------------
@@ -45,389 +22,3 @@ module.exports = utils =
params.access_token = accessToken if accessToken
"https://graph.facebook.com/#{fbId}/picture?#{$.param(params)}"
-
- # Persistent data storage
- # -----------------------
-
- # sessionStorage with session cookie fallback
- # sessionStorage(key) gets the value for 'key'
- # sessionStorage(key, value) set the value for 'key'
- sessionStorage: do ->
- if window.sessionStorage and sessionStorage.getItem and
- sessionStorage.setItem and sessionStorage.removeItem
- (key, value) ->
- if typeof value is 'undefined'
- value = sessionStorage.getItem(key)
- if value? and value.toString then value.toString() else value
- else
- sessionStorage.setItem(key, value)
- value
- else
- (key, value) ->
- if typeof value is 'undefined'
- utils.getCookie(key)
- else
- utils.setCookie(key, value)
- value
-
- # sessionStorageRemove(key) removes the storage entry for 'key'
- sessionStorageRemove: do ->
- if window.sessionStorage and sessionStorage.getItem and
- sessionStorage.setItem and sessionStorage.removeItem
- (key) -> sessionStorage.removeItem(key)
- else
- (key) -> utils.expireCookie(key)
-
- # Cookie fallback
- # ---------------
-
- # Get a cookie by its name
- getCookie: (key) ->
- keyPosition = document.cookie.indexOf "#{key}="
- return false if keyPosition is -1
- start = keyPosition + key.length + 1
- end = document.cookie.indexOf ';', start
- end = document.cookie.length if end is -1
- decodeURIComponent(document.cookie.substring(start, end))
-
- # Set a session cookie
-
- setCookie: (key, value) ->
- document.cookie = key + '=' + encodeURIComponent(value)
-
- expireCookie: (key) ->
- document.cookie = "#{key}=nil; expires=#{(new Date).toGMTString()}"
-
- # Load additonal JavaScripts
- # We don’t use jQuery here because jQuery does not attach an error
- # handler to the script. In jQuery, a proper error handler only works
- # for same-origin scripts which can be loaded via XHR.
- loadLib: (url, success, error, timeout = 7500) ->
- #console.debug 'utils.loadLib', url
- head = document.head or document.getElementsByTagName('head')[0] or
- document.documentElement
- script = document.createElement 'script'
- script.async = 'async'
- script.src = url
-
- onload = (_, aborted = false) ->
- return unless (aborted or
- not script.readyState or script.readyState is 'complete')
-
- clearTimeout timeoutHandle
-
- # Handle memory leak in IE
- script.onload = script.onreadystatechange = script.onerror = null
- # Remove the script elem and its reference
- head.removeChild(script) if head and script.parentNode
- script = undefined
-
- success() if success and not aborted
-
- script.onload = script.onreadystatechange = onload
-
- # This is what jQuery is missing
- script.onerror = ->
- onload null, true
- error() if error
-
- timeoutHandle = setTimeout script.onerror, timeout
- head.insertBefore script, head.firstChild
-
- # Functional helpers for handling asynchronous dependancies and I/O
- # -----------------------------------------------------------------
-
- ###
- Wrap methods so they can be called before a deferred is resolved.
- The actual methods are called once the deferred is resolved.
-
- Parameters:
-
- Expects an options hash with the following properties:
-
- deferred
- The Deferred object to wait for.
-
- methods
- Either:
- - A string with a method name e.g. 'method'
- - An array of strings e.g. ['method1', 'method2']
- - An object with methods e.g. {method: -> alert('resolved!')}
-
- host (optional)
- If you pass an array of strings in the `methods` parameter the methods
- are fetched from this object. Defaults to `deferred`.
-
- target (optional)
- The target object the new wrapper methods are created at.
- Defaults to host if host is given, otherwise it defaults to deferred.
-
- onDeferral (optional)
- An additional callback function which is invoked when the method is called
- and the Deferred isn't resolved yet.
- After the method is registered as a done handler on the Deferred,
- this callback is invoked. This can be used to trigger the resolving
- of the Deferred.
-
- Examples:
-
- deferMethods(deferred: def, methods: 'foo')
- Wrap the method named foo of the given deferred def and
- postpone all calls until the deferred is resolved.
-
- deferMethods(deferred: def, methods: def.specialMethods)
- Read all methods from the hash def.specialMethods and
- create wrapped methods with the same names at def.
-
- deferMethods(
- deferred: def, methods: def.specialMethods, target: def.specialMethods
- )
- Read all methods from the object def.specialMethods and
- create wrapped methods at def.specialMethods,
- overwriting the existing ones.
-
- deferMethods(deferred: def, host: obj, methods: ['foo', 'bar'])
- Wrap the methods obj.foo and obj.bar so all calls to them are postponed
- until def is resolved. obj.foo and obj.bar are overwritten
- with their wrappers.
-
- ###
- deferMethods: (options) ->
- # Process options
- deferred = options.deferred
- methods = options.methods
- host = options.host or deferred
- target = options.target or host
- onDeferral = options.onDeferral
-
- #console.debug 'utils.deferMethods', deferred, methods, host, target
-
- # Hash with named functions
- methodsHash = {}
-
- if typeof methods is 'string'
- # Transform a single method string into an object
- methodsHash[methods] = host[methods]
-
- else if methods.length and methods[0]
- # Transform a method list into an object
- for name in methods
- func = host[name]
- unless typeof func is 'function'
- throw new TypeError "utils.deferMethods: method #{name} not
-found on host #{host}"
- methodsHash[name] = func
-
- else
- # Treat methods parameter as a hash, no transformation
- methodsHash = methods
-
- # Process the hash
- for own name, func of methodsHash
- # Ignore non-function properties
- continue unless typeof func is 'function'
- # Replace method with wrapper
- target[name] = utils.createDeferredFunction(
- deferred, func, target, onDeferral
- )
-
- # Creates a function which wraps `func` and defers calls to
- # it until the given `deferred` is resolved. Pass an optional `context`
- # to determine the this `this` binding of the original function.
- # Defaults to `deferred`. The optional `onDeferral` function to after
- # original function is registered as a done callback.
- createDeferredFunction: (deferred, func, context = deferred, onDeferral) ->
- #console.debug 'utils.createWrappedFunction', 'deferred:', deferred, 'func:', func, 'context:', context, 'onDeferral:', onDeferral
- # Return a wrapper function
- ->
- # Save the original arguments
- args = arguments
- if deferred.state() is 'resolved'
- # Deferred already resolved, call func immediately
- #console.debug 'utils.createDeferredFunction: wrapped', name, 'called -> already resolved, call immediately'
- func.apply context, args
- else
- #console.debug 'utils.createDeferredFunction: wrapped', name, 'called -> defer'
- # Register a done handler
- deferred.done ->
- #console.debug 'utils.createDeferredFunction: Deferred done, call', name, args
- func.apply context, args
- # Invoke the onDeferral callback
- if typeof onDeferral is 'function'
- onDeferral.apply context
-
- # Accumulators
- accumulator:
- collectedData: {}
- handles: {}
- handlers: {}
- successHandlers: {}
- errorHandlers: {}
- interval: 2000
-
- # Turns methods into accumulators, collecting calls and sending
- # them out in intervals
- # obj
- # the object the methods are read from and written to
- # methods
- # zero or more names (strings) of methods (object members) to be wrapped
- wrapAccumulators: (obj, methods) ->
- # Replace methods
- for name in methods
- func = obj[name]
- unless typeof func is 'function'
- throw new TypeError "utils.wrapAccumulators: method #{name} not found"
- # Replace method
- obj[name] = utils.createAccumulator name, obj[name], obj
-
- # Bind to unload to synchronously flush accumulated remains
- $(window).unload =>
- handler(async: false) for name, handler of utils.accumulator.handlers
-
- # Returns an accumulator for the given 'func' with the
- # parameter list (data, success, error, options)
- createAccumulator: (name, func, context) ->
- # Create a unique ID for the function, save it as a
- # property of the function object
- unless id = func.__uniqueID
- id = func.__uniqueID = name + String(Math.random()).replace('.', '')
-
- acc = utils.accumulator
-
- # Cleanup data
- cleanup = ->
- delete acc.collectedData[id]
- delete acc.successHandlers[id]
- delete acc.errorHandlers[id]
-
- # Create accumulated success and error callbacks
-
- accumulatedSuccess = ->
- handlers = acc.successHandlers[id]
- #console.debug 'createAccumulator: accumulatedSuccess', id, handlers
- handler.apply(this, arguments) for handler in handlers if handlers
- cleanup()
-
- accumulatedError = ->
- handlers = acc.errorHandlers[id]
- #console.debug 'createAccumulator: accumulatedError', id, handlers
- handler.apply(this, arguments) for handler in handlers if handlers
- cleanup()
-
- # Resulting function
- (data, success, error, rest...) ->
- #console.debug 'accumulator', name, id, 'success:', success, 'error:', error
-
- # Store data, success and error handlers
- if data
- acc.collectedData[id] = (acc.collectedData[id] or []).concat(data)
- if success
- acc.successHandlers[id] = (
- acc.successHandlers[id] or []
- ).concat(success)
- if error
- acc.errorHandlers[id] = (acc.errorHandlers[id] or []).concat(error)
-
- # Set timeout if not already set
- return if acc.handles[id]
-
- handler = (options = options) ->
- #console.debug 'createAccumulator: handler fired'
- return unless collectedData = acc.collectedData[id]
- # Call the original function
- args = [
- collectedData, accumulatedSuccess, accumulatedError
- ].concat(rest)
- func.apply context, args
- # Clear timeout
- clearTimeout acc.handles[id]
- # Remove handles and handlers
- delete acc.handles[id]
- delete acc.handlers[id]
-
- # Save the handler
- acc.handlers[id] = handler
- # Wrap handler in additional function to ignore
- # Firefox' latency arguments
- acc.handles[id] = setTimeout (-> handler()), acc.interval
-
- # Call the given function `func` when the global event `eventType` occurs.
- # Defaults to 'login', so the `func` is called when
- # the user has successfully logged in.
- # When the function is called, `this` points to the given `context`.
- # You may pass a `loginContext` for the UI context where
- # the login was triggered.
- afterLogin: (context, func, eventType = 'login', args...) ->
- #console.debug 'utils.afterLogin', context, func, eventType, args
- if mediator.user
- # All fine, just pass through
- func.apply context, args
- else
- # Register a handler for the given event
- loginHandler = ->
- # Cleanup
- mediator.unsubscribe eventType, loginHandler
- # Pass to wrapped function
- func.apply context, args
-
- mediator.subscribe eventType, loginHandler
-
- deferMethodsUntilLogin: (obj, methods, eventType = 'login') ->
- #console.debug 'utils.deferMethodsUntilLogin', arguments...
-
- methods = [methods] if typeof methods is 'string'
-
- for name in methods
- func = obj[name]
- unless typeof func is 'function'
- throw new TypeError "utils.deferMethodsUntilLogin: method #{name}
-not found"
- #console.debug '\twrap', obj, name
- obj[name] = _(utils.afterLogin).bind null, obj, func, eventType
-
- # Delegates to afterLogin, but triggers the login dialog if the user
- # isn't logged in
- # and calls preventDefault if an event object is passed.
- ensureLogin: (context, func, loginContext, eventType = 'login', args...) ->
- #console.debug 'utils.ensureLogin', context, func, loginContext, args
- utils.afterLogin context, func, eventType, args...
-
- unless mediator.user
- # If an event is passed to the original function, prevent the
- # default action
- if (e = args[0]) and typeof e.preventDefault is 'function'
- e.preventDefault()
-
- # Start login process
- mediator.publish '!showLogin', loginContext
-
- # Wrap methods which need a logged-in user.
- # Trigger the login when they are called and there is no user.
- # Arguments:
- # `obj`: The object whose methods should be wrapped
- # `methods`: A string or an array of strings with method names
- # `loginContext`: object with login context information, should have
- # a `description` property
- # `eventType`: The global PubSub event the actual method call will wait for.
- # Defaults to 'login'.
- ensureLoginForMethods: (obj, methods, loginContext, eventType = 'login') ->
- #console.debug 'utils.ensureLoginForMethods', obj, methods, loginContext
-
- # Transform a single method string into a list
- methods = [methods] if typeof methods is 'string'
-
- for name in methods
- func = obj[name]
- unless typeof func is 'function'
- throw new TypeError "utils.ensureLoginForMethods: method #{name}
-not found"
- #console.debug '\twrap', obj, name, loginContext
- obj[name] = _(utils.ensureLogin).bind(
- null, obj, func, loginContext, eventType
- )
-
-# Seal the utils object
-Object.seal? utils
-
-# Return utils
-utils
View
55 app/lib/view_helper.coffee
@@ -1,56 +1,5 @@
-mediator = require 'mediator'
-utils = require 'lib/utils'
-
-# Registers several Handlebars helpers
-
-# Partials
-# --------
-
-Handlebars.registerHelper 'partial', (partialName, options) ->
- new Handlebars.SafeString(
- Handlebars.VM.invokePartial(
- Handlebars.partials[partialName], partialName, options.hash
- )
- )
-
-# Generators
-# ----------
-
-# Facebook image URLs
-Handlebars.registerHelper 'fb_img_url', (fbId, type) ->
- new Handlebars.SafeString utils.facebookImageURL(fbId, type)
-
-# Conditional evaluation
-# ----------------------
-
-# Choose block by user login status
-Handlebars.registerHelper 'if_logged_in', (options) ->
- if mediator.user
- options.fn(this)
- else
- options.inverse(this)
-
-# Map helpers
-# -----------
-
-# Make 'with' behave a little more mustachey
-Handlebars.registerHelper 'with', (context, options) ->
- if not context or Handlebars.Utils.isEmpty context
- options.inverse(this)
- else
- options.fn(context)
-
-# Inverse for 'with'
-Handlebars.registerHelper 'without', (context, options) ->
- inverse = options.inverse
- options.inverse = options.fn
- options.fn = inverse
- Handlebars.helpers.with.call(this, context, options)
-
-# Evaluate block with context being current user
-Handlebars.registerHelper 'with_user', (options) ->
- context = mediator.user.toJSON() or {}
- Handlebars.helpers.with.call(this, context, options)
+# Application-specific view helpers
+# ---------------------------------
Handlebars.registerHelper 'transform_if_retweeted', (options) ->
if this.retweeted_status
View
63 app/mediator.coffee
@@ -1,36 +1,27 @@
-# The mediator is the objects all others modules use to
-# communicate with each other.
-# It implements the Publish/Subscribe pattern.
-
-mediator = {}
-
-# Current user
-mediator.user = null
-
-# The router
-mediator.router = null
-
-# Include Backbone event methods for
-# global Publish/Subscribe
-_(mediator).defaults Backbone.Events
-
-# Initialize an empty callback list (so we might seal the object)
-mediator._callbacks = null
-
-# Create Publish/Subscribe aliases
-mediator.subscribe = mediator.on = Backbone.Events.on
-mediator.unsubscribe = mediator.off = Backbone.Events.off
-mediator.publish = mediator.trigger = Backbone.Events.trigger
-
-# Make subscribe, unsubscribe and publish properties readonly
-if Object.defineProperties
- desc = writable: false
- Object.defineProperties mediator,
- subscribe: desc, unsubscribe: desc, publish: desc
-
-# Seal the mediator object
-# (extensible: false for the mediator, configurable: false for its properties)
-Object.seal? mediator
-
-# Return mediator
-module.exports = mediator
+createMediator = require 'chaplin/lib/create_mediator'
+
+# Mediator singleton
+# ------------------
+
+# The mediator is a simple object all others modules use to
+# communicate with each other. It implements the Publish/Subscribe pattern.
+#
+# Additionally, it holds two common objects which need to be shared
+# between modules: the user and the router.
+#
+# This module returns the mediator singleton object. This is the
+# application-wide mediator you might load into modules
+# which need to talk to other modules using Publish/Subscribe.
+#
+# The actual creation of the mediator takes place in another
+# module, see chaplin/lib/create_mediator.coffee.
+
+# Create the mediator using Chaplin’s constructor,
+# add properties/methods for getting/setting the user and the router
+module.exports = mediator = createMediator
+ createRouterProperty: true
+ createUserProperty: true
+
+# You might add properties to the mediator here
+
+#mediator.foo = ->
View
92 app/models/collection.coffee
@@ -1,91 +1,3 @@
-Subscriber = require 'lib/subscriber'
+ChaplinCollection = require 'chaplin/models/collection'
-# Abstract class which extends the standard Backbone collection
-# in order to add some functionality
-module.exports = class Collection extends Backbone.Collection
- # Mixin a Subscriber
- _(Collection.prototype).defaults Subscriber
-
- #initialize: ->
- ##console.debug 'Collection#initialize'
- #super
- # TODO: Remove an item if a 'dispose' events bubbles and
- # it wasn't removed before?
-
- # Adds a collection atomically, i.e. throws no event until
- # all members have been added
-
- addAtomic: (models, options = {}) ->
- return unless models.length
- options.silent = true
- batch_direction = if typeof options.at is 'number' then 'pop' else 'shift'
- @add(model, options) while model = models[batch_direction]()
- @trigger 'reset'
-
- # Updates a collection with a list
- # Just like the reset method, but only adds new items and
- # removes items which are not in the new list
- #
- # options:
- # deep: Boolean flag to specify whether existing models should be updated
- # with new values
- update: (newList, options = {}) ->
- #console.debug 'Collection#update', 'deep?', options.deep
-
- fingerPrint = @pluck('id').join()
- ids = _(newList).pluck('id')
- newFingerPrint = ids.join()
- #console.debug '\t' + fingerPrint + '\n\t' + newFingerPrint + '\n\t' + (fingerPrint is newFingerPrint)
-
- # Only execute removal if ID fingerprints differ
- unless fingerPrint is newFingerPrint
- # Remove items which are not in the new list
- _ids = _(ids) # Underscore wrapper
-
- i = @models.length - 1
- while i >= 0
- model = @models[i]
- unless _ids.include model.id
- #console.debug '\tremove', model.id
- @remove model
- i--
-
- # Only add/update list if ID fingerprints differ or update
- # is deep (member attributes)
- unless fingerPrint is newFingerPrint and not options.deep
- # Add item which are not yet in the list
- for model, i in newList
- preexistent = @get model.id
- if preexistent
- continue unless options.deep
- #console.debug 'update', preexistent.id
- preexistent.set model
- else
- #console.debug '\tinsert', model.id, 'at', i
- @add model, at: i
-
- # Disposal
- # --------
-
- disposed: false
-
- dispose: ->
- return if @disposed
- #console.debug 'Collection#dispose', this
-
- # Fire an event to notify associated views
- @trigger 'dispose', this
-
- # Unbind all global event handlers
- @unsubscribeAllEvents()
-
- # Empty the list silently, but do not dispose all models since
- # they might be referenced elsewhere
- @reset [], silent: true
-
- # Finished
- #console.debug 'Collection#dispose', this, 'finished'
- @disposed = true
-
- # Your're frozen when your heart’s not open
- Object.freeze? this
+module.exports = class Collection extends ChaplinCollection
View
41 app/models/model.coffee
@@ -1,40 +1,3 @@
-Subscriber = require 'lib/subscriber'
+ChaplinModel = require 'chaplin/models/model'
-module.exports = class Model extends Backbone.Model
- # Mixin a Subscriber
- _(Model.prototype).defaults Subscriber
-
- # This method is used to get the attributes for the view template
- # and might be overwritten by decorators which cannot create a
- # proper `attributes` getter due to ECMAScript 3 limits.
- getAttributes: ->
- @attributes
-
- # Disposal
- # --------
-
- disposed: false
-
- dispose: ->
- return if @disposed
- #console.debug 'Model#dispose', this
-
- # Fire an event to notify associated collections and views
- @trigger 'dispose', this
-
- # Unbind all global event handlers
- @unsubscribeAllEvents()
-
- # Remove the collection reference, attributes and event handlers
- properties = [
- 'collection', 'attributes', '_escapedAttributes',
- '_previousAttributes', '_callbacks'
- ]
- delete @[prop] for prop in properties
-
- # Finished
- #console.debug 'Model#dispose', this, 'finished'
- @disposed = true
-
- # Your're frozen when your heart’s not open
- Object.freeze? this
+module.exports = class Model extends ChaplinModel
View
2  app/models/navigation.coffee
@@ -1,4 +1,4 @@
-Model = require 'models/model'
+Model = require './model'
module.exports = class Navigation extends Model
defaults:
View
2  app/models/status.coffee
@@ -1,5 +1,5 @@
mediator = require 'mediator'
-Model = require 'models/model'
+Model = require './model'
module.exports = class Status extends Model
minLength: 1
View
2  app/models/tweet.coffee
@@ -1,3 +1,3 @@
-Model = require 'models/model'
+Model = require './model'
module.exports = class Tweet extends Model
View
4 app/models/tweets.coffee
@@ -1,6 +1,6 @@
mediator = require 'mediator'
-Collection = require 'models/collection'
-Tweet = require 'models/tweet'
+Collection = require './collection'
+Tweet = require './tweet'
module.exports = class Tweets extends Collection
model: Tweet
View
4 app/models/user.coffee
@@ -1,10 +1,10 @@
-Model = require 'models/model'
mediator = require 'mediator'
+Model = require './model'
module.exports = class User extends Model
initialize: ->
super
- mediator.bind 'userMethods', @initializeMethods
+ mediator.on 'userMethods', @initializeMethods
# twttr.anywhere has many useful methods like isFollowedBy()
# so it's great to have them in the model.
View
4 app/routes.coffee
@@ -0,0 +1,4 @@
+module.exports = (match) ->
+ match '', 'tweets#index'
+ match '@:user', 'user#show'
+ match 'logout', 'navigation#logout'
View
320 app/views/application_view.coffee
@@ -1,320 +0,0 @@
-mediator = require 'mediator'
-utils = require 'lib/utils'
-
-module.exports = class ApplicationView # Do not inherit from View
-
- # Set your application name here so
- # the document title is set properly to
- # “Site title – Controller title” (see adjustTitle)
- siteTitle = 'Tweet your brunch'
-
- previousController: null
-
- # The current controller, its name, main view and parameters
- currentControllerName: null
- currentController: null
- currentAction: null
- currentView: null
- currentParams: null
-
- # The current URL
- url: null
-
- constructor: ->
- @logout() unless mediator.user
-
- # Listen to global events
- mediator.subscribe 'matchRoute', @matchRoute
- mediator.subscribe '!startupController', @startupController
- mediator.subscribe 'login', @login
- mediator.subscribe 'logout', @logout
- mediator.subscribe 'startupController', @removeFallbackContent
-
- @addGlobalHandlers()
-
- #
- # Handlers for user login / logout
- #
-
- # Handler for the global login event
-
- login: (user) =>
- #console.debug 'ApplicationView#login', user
-
- $(document.body)
- # Switch login state classes
- .removeClass('logged-out')
- .addClass('logged-in')
-
- # Handler for the global logout event
-
- logout: =>
- #console.debug 'ApplicationView#logout'
- $(document.body)
- # Switch login state classes
- .removeClass('logged-in')
- .addClass('logged-out')
-
- #
- # Controller management
- # Starting controllers, showing and hiding views
- #
-
- # Handler for the global matchRoute event
-
- matchRoute: (route, params) =>
- #console.debug 'ApplicationView#matchRoute', route, params
- controllerName = route.controller
- action = route.action
- @startupController controllerName, action, params
-
- # Handler for the global !startupController event
- #
- # The standard flow is:
- #
- # 1. Test if it’s a new controller/action with new params
- # 1. Hide the old view
- # 2. Destroy the old controller
- # 3. Instantiate the new controller, call the controller action
- # 4. Show the new view
-
- startupController: (controllerName, action = 'index', params = {}) =>
- #console.debug "ApplicationView#startupController\t#{@currentControllerName}##{@currentAction} > #{controllerName}##{action}\tparams", params
-
- # Set default flags
-
- # Whether to update the URL after controller startup
- # Default to true unless explicitly set to false
- params.changeURL = true unless params.changeURL is false
-
- # Whether to force the controller startup even
- # when current and new controllers and params match
- params.forceStartup = false unless params.forceStartup is true
-
- # Check if the desired controller is already active
- sameController = not params.forceStartup and
- @currentControllerName is controllerName and
- @currentAction is action and
- # Deep parameters check is not nice but the simplest way for now
- (not @currentParams or _(params).isEqual(@currentParams))
-
- #console.debug 'ApplicationView#startupController sameController?', sameController
-
- # Stop if it’s the same controller/action with the same params
- if sameController
- #console.debug "ApplicationView#startupController: #{controllerName}##{action} already active with same parameters"
- return
-
- # Fetch the new controller, then go on
- controllerFileName = utils.underscorize(controllerName) + '_controller'
- controller = require "controllers/#{controllerFileName}"
- @controllerLoaded controllerName, action, params, controller
-
- # Handler for the controller lazy-loading
-
- controllerLoaded: (controllerName, action, params, ControllerConstructor) ->
- #console.debug 'ApplicationView#controllerLoaded', controllerName, action, params, ControllerConstructor
-
- # Shortcuts for the old controller
- currentControllerName = @currentControllerName or null
- currentController = @currentController or null
- currentView = @currentController.view if @currentController
-
- # Jump to the top of the page
- scrollTo 0, 0
-
- # Hide the container element of the current view
- if currentView and currentView.$container
- currentView.$container.css 'display', 'none'
-
- # Dispose the current controller
- if currentController
- unless typeof currentController.dispose is 'function'
- throw new Error "ApplicationView#controllerLoaded: dispose method
-not found on #{currentControllerName} controller"
- # Passing the params and the new controller name
- currentController.dispose params, controllerName
-
- # Initialize the new controller
- controller = new ControllerConstructor()
-
- # Call the initialize method
- # Passing the params and the old controller name
- controller.initialize params, currentControllerName
-
- # Call the specific controller action
- unless typeof controller[action] is 'function'
- throw new Error "ApplicationView#controllerLoaded: action #{action}
-not found on #{controllerName} controller"
- controller[action] params, currentControllerName
-
- # Show the container element of the new view
- view = controller.view
- if view and view.$container
- view.$container.css display: 'block', opacity: 1
-
- # Save the new controller
- @previousController = currentControllerName
- @currentControllerName = controllerName
- @currentController = controller
- @currentAction = action
- @currentView = view
- @currentParams = params
-
- # Change the URL to the new controller
- @adjustURL()
-
- # Change the document title to match the current controller
- @adjustTitle()
-
- # We're done! Publish a global startupController event
- # with these parameters:
- # - name of the new controller
- # - params for the new controllre
- # - name of the old controller
- #console.debug 'ApplicationView#startupController: publish startupController', @currentControllerName, @currentParams, @previousController
- mediator.publish 'startupController', @currentControllerName,
- @currentParams, @previousController
-
- # Change the URL to the new controller using the Backbone Router
-
- adjustURL: ->
- # Shortcuts
- controller = @currentController
- params = @currentParams
-
- #console.debug 'ApplicationView#adjustURL', controller, params
-
- if typeof controller.historyURL is 'function'
- # If the property is a function, call it
- historyURL = controller.historyURL params
-
- else if typeof controller.historyURL is 'string'
- historyURL = controller.historyURL
-
- else
- throw new Error "ApplicationView#adjustURL: controller for
-#{controller} does not provide a historyURL"
-
- # Pass to the router to actually change the current URL
- # (call history.pushState)
- if params.changeURL
- mediator.router.changeURL historyURL
-
- # Save the URL
- @url = historyURL
-
- # Change the document title. Get the title from the title property
- # of the params or of the current controller
-
- adjustTitle: ->
- # You might change this if you want the opposite order of
- # the controller and site titles
- title = siteTitle
- subtitle = @currentParams.title or @currentController.title
- title += " \u2013 #{subtitle}" if subtitle
- # Internet Explorer < 9 workaround
- setTimeout (-> document.title = title), 50
-
- #
- # Fallback content
- #
-
- # After the first controller has been started, remove all accessible
- # content so the DOM is less complex and images and video do not lie
- # in the background
-
- removeFallbackContent: =>
- # Hide the accessible fallback and the loading screen
- $('#startup-loading, .accessible-fallback').remove()
-
- # Remove the handler after the first startupController event
- mediator.unsubscribe 'startupController', @removeFallbackContent
-
- #
- # Event handling
- #
-
- # Global event handlers
-
- addGlobalHandlers: ->
- # Handle links
- $(document)
- .delegate('#logout-button', 'click', @logoutButtonClick)
- .delegate('.go-to', 'click', @goToHandler)
- .delegate('a', 'click', @openLink)
-
- # Handle all clicks on A elements and try to route them internally
-
- openLink: (event) =>
- #console.debug 'AppView#openLink'
- el = event.currentTarget
-
- # Handle empty href
- hrefAttr = el.getAttribute 'href'
- #console.debug '\threfAttr »' + hrefAttr + '«'
- return if hrefAttr is '' or /^#/.test(hrefAttr)
-
- # Is it an external link?
- href = el.href
- hostname = el.hostname
- #console.debug '\thref »' + href + '«'
- #console.debug '\thostname »' + hostname + '«'
- return unless href and hostname
- currentHostname = location.hostname.replace('.', '\\.')
- hostnameRegExp = ///#{currentHostname}$///i
- external = not hostnameRegExp.test(hostname)
- #console.debug '\texternal?', external
- if external
- # Open external links normally
- # You might want to enforce opening in a new tab here:
- #event.preventDefault()
- #window.open href
- return
-
- @openInternalLink event
-
- # Try to route a click on a link internally
-
- openInternalLink: (event) ->
- #console.debug 'AppView#openInternalLink'
- event.preventDefault()
- el = event.currentTarget
-
- path = el.pathname
- #console.debug '\tpath »' + path + '«'
- return unless path
-
- # Pass to the router. Returns true if the URL could be routed.
- result = mediator.router.route path
- #console.debug '\tfollow result', result
-
- event.preventDefault() if result
-
- # Not only A elements might act as internal links,
- # every element might have:
- # class="go-to" data-href="/something"
-
- goToHandler: (event) ->
- el = event.currentTarget
- #console.debug 'AppView#goToHandler', el, event.nodeName, $(el).data('href')
-
- # Do not handle A elements
- return if event.nodeName is 'A'
-
- path = $(el).data('href')
- return unless path
-
- # Pass to the router. Returns true if the URL could be routed.
- result = mediator.router.route path
- #console.debug '\tfollow result', result
-
- event.preventDefault() if result
-
- # Handle clicks on the logout button
-
- logoutButtonClick: (event) ->
- event.preventDefault()
-
- # Publish a global !logout event
- mediator.publish '!logout'
View
368 app/views/collection_view.coffee
@@ -1,368 +0,0 @@
-utils = require 'lib/utils'
-View = require 'views/view'
-
-# General class for rendering Collections. Inherit from this class and
-# overwrite at least getView. getView gets an item model
-# and should instantiate a corresponding item view.
-module.exports = class CollectionView extends View
-
- # Animation duration when adding new items (set to 0 to disable fade in)
- animationDuration: 500
-
- # Hash which saves all item views by CID
- viewsByCid: null
-
- # The list element the item views are actually appended to.
- # If empty, $el is used.
- # Set the selector property in the derived class to use a specific element.
- listSelector: null
- $list: null
-
- # Selector for a fallback element which is shown if the collection is empty.
- # Set the selector property in the derived class to use a specific element.
- fallbackSelector: null
- $fallback: null
-
- # Selector which identifies child elements belonging to collection
- # All children are seen as belonging to collection if not present
- itemSelector: null
-
- # Track a list of the visible views
- visibleItems: null
-
- # Returns an instance of the view class
- # This method has to be overridden by a derived class.
- # This is not simply a property with a constructor so that
- # several item view constructors are possible depending
- # on the item model type.
- getView: ->
- throw new Error 'CollectionView#getView must be overridden'
-
- initialize: (options = {}) ->
- super
- #console.debug 'CollectionView#initialize', this, @collection, options
-
- # Default options
- _(options).defaults
- render: true # Render the view immediately per default
- renderItems: true # Render all items immediately per default
- filterer: null # No filter function
-
- # Initialize lists for views and visible items
- @viewsByCid = {}
- @visibleItems = []
-
- # Start observing the model
- @addCollectionListeners()
-
- # Set the filter function
- @filter options.filterer if options.filterer
-
- # Render template once
- @render() if options.render
-
- # Render all items initially
- @renderAllItems() if options.renderItems
-
- # Binding of collection listeners
- addCollectionListeners: ->
- @modelBind 'loadStart', @showLoadingIndicator
- @modelBind 'load', @hideLoadingIndicator
- @modelBind 'add', @itemAdded
- @modelBind 'remove', @itemRemoved
- @modelBind 'reset', @itemsResetted
-
- # Generic loading indicator
- showLoadingIndicator: =>
- # Only show the loading indicator if the collection is empty
- # (otherwise the pagination should show a loading indicator)
- return if @collection.length
- @$('.loading').css 'display', 'block'
-
- hideLoadingIndicator: =>
- @$('.loading').css 'display', 'none'
-
- # Adding / Removing
- # -----------------
-
- # When an item is added, create a new view and insert it
- itemAdded: (item, collection, options = {}) =>
- #console.debug 'CollectionView#itemAdded', this, item.cid, item
- @renderAndInsertItem item, options.index
-
- # When an item is removed, remove the corresponding view from DOM and caches
- itemRemoved: (item) =>
- #console.debug 'CollectionView#itemRemoved', this, item.cid, item
- @removeViewForItem item
-
- # When all items are resetted, render all anew
- itemsResetted: =>
- #console.debug 'CollectionView#itemsResetted', this, @collection.length, @collection.models
- @renderAllItems()
-
- # Main render method (should be called only once)
- render: ->
- super
- #console.debug 'CollectionView#render', this, @collection
-
- # Set the $list property
- @$list = if @listSelector then @$(@listSelector) else @$el
-
- @initFallback()
-
- #
- # Fallback message when the collection is empty
- #
-
- # Initialize the fallback
- initFallback: ->
- return unless @fallbackSelector
-
- #console.debug 'CollectionView#initFallback', this, @el, @el.parentNode
-
- # Set the $fallback property
- @$fallback = @$(@fallbackSelector)
-
- # The collection has to be a deferred in order that
- # the fallback can be shown properly
- f = 'function'
- isDeferred = (typeof @collection.done is f and
- typeof @collection.state is f)
- return unless isDeferred
-
- # Listen for visible items changes
- @bind 'visibilityChange', @showHideFallback
-
- # Show or hide the fallback when the visible items change
- showHideFallback: =>
- #console.debug 'CollectionView#showHideFallback', this, 'visibleItems', @visibleItems, 'collection', @collection
- # Show fallback message if no item is visible and
- # the collection Deferred has been resolved
- empty = @visibleItems.length is 0 and @collection.state() is 'resolved'
- @$fallback.css 'display', if empty then 'block' else 'none'
-
- # Render and insert all items
- # Accepts the options `shuffle` (Boolean) and `limit` (Number)
- renderAllItems: (options = {}) =>
-
- items = @collection.models
- #console.debug 'CollectionView#renderAllItems', items.length
-
- # Shuffle
- if options.shuffle
- items = MovieExplorer.utils.shuffle @collection.models
-
- # Apply limit
- if options.limit
- items = items.slice(0, options.limit)
-
- # Reset visible items
- @visibleItems = []
-
- # Collect remaining views
- remainingViewsByCid = {}
- for item in items
- view = @viewsByCid[item.cid]
- if view
- #console.debug '\tview for', item.cid, 'remains'
- remainingViewsByCid[item.cid] = view
-
- # Remove old views of items not longer in the list
- for own cid, view of @viewsByCid
- #console.debug '\tcheck', cid, view, 'remaining?', cid of remainingViewsByCid
- unless cid of remainingViewsByCid
- #console.debug '\t\tremove view for', cid
- @removeView cid, view
-
- # Re-insert remaining items; render and insert new items
- #console.debug '\tbuild up list again'
- for item, index in items
- # View already created?
- view = @viewsByCid[item.cid]
- if view
- # Re-insert the view
- #console.debug '\tre-insert', item.cid
- @insertView item, view, index, 0
- else
- # Create a new view, render and insert it
- #console.debug '\trender and insert new view for', item.cid
- @renderAndInsertItem item, index
-
- # If no view was created, trigger `visibilityChange` event manually
- unless items.length
- #console.debug 'CollectionView#renderAllItems', 'visibleItems', @visibleItems.length
- @trigger 'visibilityChange', @visibleItems
-
- # Applies a filter to the collection.
- # Expects an interator function as parameter.
- # Hides all items for which the iterator returns false.
- filter: (filterer) ->
- #console.debug 'CollectionView#filter', this, @collection
-
- # Save the new filterer function
- @filterer = filterer
-
- # Show/hide existing views
- unless _(@viewsByCid).isEmpty()
- for item, index in @collection.models
-
- # Apply filter to the item
- included = if filterer then filterer(item, index) else true
-
- # Show/hide the view accordingly
- view = @viewsByCid[item.cid]
- # A view has not been created for this item yet
- unless view
- #console.debug 'CollectionView#filter: no view for', item.cid, item
- continue
-
- #console.debug item, item.cid, view
- $(view.el).stop(true, true)[if included then 'show' else 'hide']()
-
- # Update visibleItems list, but do not trigger an event immediately
- @updateVisibleItems item, included, false
-
- # Trigger a combined `visibilityChange` event
- #console.debug 'CollectionView#filter', 'visibleItems', @visibleItems.length
- @trigger 'visibilityChange', @visibleItems
-
- # Render the view for an item
- renderAndInsertItem: (item, index) ->
- #console.debug 'CollectionView#renderAndInsertItem', item.cid, item
-
- view = @renderItem item
- @insertView item, view, index
-
- # Instantiate and render an item using the viewsByCid hash as a cache
- renderItem: (item) ->
- #console.debug 'CollectionView#renderItem', item.cid, item
-
- # Get the existing view
- view = @viewsByCid[item.cid]
-
- # Instantiate a new view by calling getView if necessary
- unless view
- view = @getView(item)
- # Save the view in the viewsByCid hash
- @viewsByCid[item.cid] = view
-
- # Render in any case
- view.render()
-
- view
-
- # Inserts a view into the list at the proper position
- insertView: (item, view, index = null, animationDuration = @animationDuration) ->
- #console.debug 'CollectionView#insertView', item, view, index
-
- # Get the insertion offset
- position = if typeof index is 'number'
- index
- else
- @collection.indexOf(item)
- #console.debug '\titem', item.id, 'position', position, 'length', @collection.length
-
- # Is the item included in the filter?
- included = if @filterer then @filterer(item, position) else true
- #console.debug '\tincluded?', included
-
- # Get the view’s top element
- $viewEl = view.$el
-
- if included
- # Make view transparent if animation is enabled
- $viewEl.css 'opacity', 0 if animationDuration
- else
- # Hide the view if it’s filtered
- $viewEl.css 'display', 'none'
-
- # Get the lsit and its children which are originate from item views
- $list = @$list
- children = $list.children(@itemSelector)
-
- if position is 0
- # Insert at the beginning
- #console.debug '\tinsert at the beginning'
- $list.prepend($viewEl)
- else if position < children.length
- # Insert at the right position
- #console.debug '\tinsert before', children.eq(position)
- children.eq(position).before($viewEl)
- else
- # Insert at the end
- #console.debug '\tinsert at the end'
- $list.append($viewEl)
-
- # Tell the view that it was added to the DOM
- view.trigger 'addedToDOM'
-
- # Update the list of visible items, fire a `visibilityChange` event
- @updateVisibleItems item, included
-
- # Fade the view in if it was made transparent before
- if animationDuration and included
- $viewEl.animate {opacity: 1}, animationDuration
-
- # Remove the view for an item
- removeViewForItem: (item) ->
- #console.debug 'CollectionView#removeViewForItem', this, item
-
- # Remove item from visibleItems list
- @updateVisibleItems item, false
-
- # Get the view
- view = @viewsByCid[item.cid]
-
- @removeView item.cid, view
-
- # Remove a view
- removeView: (cid, view) ->
- #console.debug 'CollectionView#removeView', cid, view
-
- # Dispose the view
- view.dispose()
-
- # Remove the view from the hash table
- delete @viewsByCid[cid]
-
- # Update visibleItems list and trigger a `visibilityChanged` event
- # if an item changed its visibility
- updateVisibleItems: (item, includedInFilter, triggerEvent = true) ->
- visibilityChanged = false
-
- visibleItemsIndex = _(@visibleItems).indexOf item
- includedInVisibleItems = visibleItemsIndex > -1
- #console.debug 'CollectionView#updateVisibleItems', @collection.constructor.name, item.id, 'included?', includedInFilter
-
- if includedInFilter and not includedInVisibleItems
- # Add item to the visible items list
- @visibleItems.push item
- visibilityChanged = true
-
- else if not includedInFilter and includedInVisibleItems
- # Remove item from the visible items list
- @visibleItems.splice visibleItemsIndex, 1
- visibilityChanged = true
-
- #console.debug '\tvisibilityChanged?', visibilityChanged, 'visibleItems', @visibleItems.length, 'triggerEvent?', triggerEvent
-
- # Trigger a `visibilityChange` event if the visible items changed
- if visibilityChanged and triggerEvent
- @trigger 'visibilityChange', @visibleItems
-
- visibilityChanged
-
- # Remove the whole list from DOM
- dispose: =>
- #console.debug 'CollectionView#dispose', this, 'disposed?', @disposed
- return if @disposed
-
- # Dispose all item views
- view.dispose() for own cid, view of @viewsByCid
-
- # Remove jQuery object, item view cache and reference to collection
- properties = '$list viewsByCid visibleItems'.split(' ')
- delete @[prop] for prop in properties
-
- # Self-disposal
- super
View
4 app/views/login_view.coffee
@@ -1,12 +1,12 @@
mediator = require 'mediator'
utils = require 'lib/utils'
-View = require 'views/view'
+View = require './view'
template = require './templates/login'
module.exports = class LoginView extends View
# This is a workaround.
# In the end you might want to used precompiled templates.
- @template = template
+ template: template
id: 'login'
containerSelector: '#content-container'
View
4 app/views/navigation_view.coffee
@@ -1,11 +1,11 @@
mediator = require 'mediator'
-View = require 'views/view'
+View = require './view'
template = require './templates/navigation'
module.exports = class NavigationView extends View
# This is a workaround.
# In the end you might want to used precompiled templates.
- @template: template
+ template: template
id: 'navigation'
containerSelector: '#navigation-container'
View
5 app/views/sidebar_view.coffee
@@ -5,10 +5,7 @@ StatusView = require './status_view'
template = require './templates/sidebar'
module.exports = class SidebarView extends CompositeView
-
- # This is a workaround.
- # In the end you might want to used precompiled templates.
- @template = template
+ template: template
id: 'sidebar'
containerSelector: '#sidebar-container'
View
4 app/views/stats_view.coffee
@@ -1,9 +1,9 @@
mediator = require 'mediator'
-View = require 'views/view'
+View = require './view'
template = require './templates/stats'
module.exports = class StatsView extends View
- @template: template
+ template: template
className: 'stats'
tagName: 'ul'
containerSelector: '#stats-container'
View
4 app/views/status_view.coffee
@@ -1,10 +1,10 @@
mediator = require 'mediator'
Status = require 'models/status'
-View = require 'views/view'
+View = require './view'
template = require './templates/status'
module.exports = class StatusView extends View
- @template: template
+ template: template
id: 'status'
className: 'status'
containerSelector: '#status-container'
View
5 app/views/tweet_view.coffee
@@ -1,7 +1,6 @@
template = require './templates/tweet'
-View = require 'views/view'
+View = require './view'
module.exports = class TweetView extends View
- @template = template
-
+ template: template
className: 'tweet'
View
12 app/views/tweets_view.coffee
@@ -1,9 +1,10 @@
mediator = require 'mediator'
-CollectionView = require 'views/collection_view'
-TweetView = require 'views/tweet_view'
+CollectionView = require 'chaplin/views/collection_view'
+TweetView = require './tweet_view'
+template = require './templates/tweets'
module.exports = class TweetsView extends CollectionView
- @template = require './templates/tweets'
+ template: template
tagName: 'div' # This is not directly a list but contains a list
id: 'tweets'
@@ -27,5 +28,10 @@ module.exports = class TweetsView extends CollectionView
@$('.tweets, .tweets-header').css 'display', if mediator.user then 'block' else 'none'
render: ->
+ console.log 'TweetsView#render', this, @$el
super
@showHideLoginNote()
+
+ afterRender: ->
+ super
+ console.log 'TweetsView#afterRender', @containerSelector, $(@containerSelector)
View
296 app/views/view.coffee
@@ -1,295 +1,3 @@
-utils = require 'lib/utils'
-Subscriber = require 'lib/subscriber'
-require 'lib/view_helper'
+ChaplinView = require 'chaplin/views/view'
-module.exports = class View extends Backbone.View
-
- # Mixin a Subscriber
- _(View.prototype).defaults Subscriber
-
- # Automatic rendering
- # Flag whether to render the view automatically on initialization.
- # As an alternative you might pass a `render` option to the constructor.
- autoRender: false
-
- # Automatic appending to DOM
- # View container element
- # Set this property in a derived class to specify to selector
- # of the container element. The view is automatically appended
- # to the container when it’s rendered.
- # As an alternative you might pass a `container` option to the constructor.
- containerSelector: null
- $container: null
-
- constructor: ->
- #console.debug 'View#constructor', this
-
- # Wrap `initialize` and `render` in order to call `afterInitialize`
- # and `afterRender`
- instance = this
- wrapMethod = (name) ->
- # TODO: This isn’t so nice because it creates wrappers on each
- # instance which leads to many function objects.
- # A better way would be using Object.getPrototypeOf to look for a
- # prototype in the chain which has a overriding method.
- # For now, get the method using the prototype chain and
- # wrap it on the instance.
- func = instance[name]
- # Create a method on the instance which wraps the inherited
- instance[name] = ->
- #console.debug 'View#' + name + ' wrapper', this
- # Call the original method
- func.apply instance, arguments
- # Call the corresponding `after~` method
- instance["after#{utils.upcase(name)}"].apply instance, arguments
-
- wrapMethod 'initialize'
- wrapMethod 'render'
-
- # Finally call Backbone’s constructor
- super
-
- initialize: (options) ->
- #console.debug 'View#initialize', this, 'options', options
- # No super call here, Backbone’s `initialize` is a no-op
-
- # Listen for disposal of the model
- # If the model is disposed, automatically dispose the associated view
- if @model or @collection
- @modelBind 'dispose', @dispose
-
- # Create a shortcut to the container element
- # The view will be automatically appended to the container when rendered
- if options and options.container
- @$container = $(container)
- else if @containerSelector
- @$container = $(@containerSelector)
-
- # This method is called after a specific `initialize` of a derived class
- afterInitialize: (options) ->
- #console.debug 'View#afterInitialize', this, 'options', options
-
- # Render automatically if set by options or instance property
- # and the option do not override it
- byOption = options and options.autoRender is true
- byDefault = @autoRender and not byOption
- @render() if byOption or byDefault
-
- # Make delegateEvents defunct, it is not used in our approach
- # but is called by Backbone internally
- delegateEvents: ->
- # Noop
-
- # Setup a simple one-way model-view binding
- # Pass changed values from model to specific elements in the view
- pass: (eventType, selector) ->
- model = @model or @collection
- @modelBind eventType, (model, val) =>
- @$(selector).html(val)
-
- # User input event handling
- # -------------------------
-
- # Event handling using event delegation
- # Register a handler for a specific event type
- # For the whole view:
- # delegate(eventType, handler)
- # e.g.
- # @delegate('click', @clicked)
- # For an element in the passing a selector:
- # delegate(eventType, selector, handler)
- # e.g.
- # @delegate('click', 'button.confirm', @confirm)
- delegate: (eventType, second, third) ->
- if typeof eventType isnt 'string'
- throw new TypeError 'View#delegate: first argument must be a string'
-
- if arguments.length is 2
- handler = second
- else if arguments.length is 3
- selector = second
- if typeof selector isnt 'string'
- throw new TypeError 'View#delegate: second argument must be a string'
- handler = third
- else
- throw new TypeError 'View#delegate: only two or three arguments are
-allowed'
-
- if typeof handler isnt 'function'
- throw new TypeError 'View#delegate: handler argument must be function'
-
- # Add an event namespace
- eventType += ".delegate#{@cid}"
-
- # Bind the handler to the view
- handler = _(handler).bind(this)
-
- if selector
- # Register handler
- @$el.on eventType, selector, handler
- else
- # Register handler
- @$el.on eventType, handler
-
- # Remove all handlers registered with @delegate
-
- undelegate: ->
- @$el.unbind ".delegate#{@cid}"
-
- # Model binding
- # -------------
-
- # The following implementation resembles subscriber.coffee
-
- # Bind to a model event
- modelBind: (type, handler) ->
- if typeof type isnt 'string'
- throw new TypeError 'View#modelBind: type argument must be string'
- if typeof handler isnt 'function'
- throw new TypeError 'View#modelBind: handler argument must be function'
- model = @model or @collection
- return unless model
- @modelBindings or= {}
- handlers = @modelBindings[type] or= []
- return if _(handlers).include handler
- handlers.push handler
- model.bind type, handler
-
- # Unbind from a model event
- modelUnbind: (type, handler) ->
- if typeof type isnt 'string'
- throw new TypeError 'View#modelUnbind: type argument must be string'
- if typeof handler isnt 'function'
- throw new TypeError 'View#modelUnbind: handler argument must be
-function'
- return unless @modelBindings
- handlers = @modelBindings[type]
- if handlers
- index = _(handlers).indexOf handler
- handlers.splice index, 1 if index > -1
- delete @modelBindings[type] if handlers.length is 0
- model = @model or @collection
- return unless model
- model.unbind type, handler
-
- # Unbind all recorded global handlers
- modelUnbindAll: () ->
- return unless @modelBindings
- model = @model or @collection
- return unless model
- for own type, handlers of @modelBindings
- for handler in handlers
- model.unbind type, handler
- @modelBindings = null
-
- # Rendering
- # ---------
-
- # Get attributes from model or collection
- getTemplateData: ->
-
- modelAttributes = @model and @model.getAttributes()
- templateData = if modelAttributes
- # Return an object which delegates to the returned attributes
- # object so a custom getTemplateData might safely add and alter
- # properties (at least primitive values).
- utils.beget modelAttributes
- else
- {}
-
- # If the model is a Deferred, add a flag to get the Deferred state
- if @model and typeof @model.state is 'function'
- templateData.resolved = @model.state() is 'resolved'
-
- templateData
-
- # Main render function
- # Always bind it to the view instance
- render: =>
- # console.debug "View#render\n\t", this, "\n\tel:", @el, "\n\tmodel/collection:", (@model or @collection), "\n\tdisposed:", @disposed
-
- return if @disposed
-
- # Template compilation
-
- # In the end, you will want to precompile the templates to JavaScript
- # functions on the server-side and just load the compiled JavaScript
- # code. In this demo, we load the template as a string, compile it
- # on the client-side and store it on the view constructor as a
- # static property.
-
- template = @constructor.template
- #console.debug "\ttemplate: #{typeof template}"
-
- if typeof template is 'string'
- template = Handlebars.compile template
- # Save compiled template
- @constructor.template = template
-
- if typeof template