appetizer-ui 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,28 @@
1
+ # Appetizer UI
2
+
3
+ An painfully under-documented and opinionated Appetizer add-on for
4
+ writing webapps using Sinatra, Sass, CoffeeScript, Eco, Backbone.js,
5
+ and Sprockets.
6
+
7
+ ## License (MIT)
8
+
9
+ Copyright 2011 Audiosocket (tech@audiosocket.com)
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining
12
+ a copy of this software and associated documentation files (the
13
+ 'Software'), to deal in the Software without restriction, including
14
+ without limitation the rights to use, copy, modify, merge, publish,
15
+ distribute, sublicense, and/or sell copies of the Software, and to
16
+ permit persons to whom the Software is furnished to do so, subject to
17
+ the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be
20
+ included in all copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
23
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
26
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
27
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
28
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.authors = ["Audiosocket"]
3
+ gem.email = ["tech@audiosocket.com"]
4
+ gem.description = "A painfully opinionated Appetizer extension for web apps."
5
+ gem.summary = "Helpers for rich clients using Sinatra, Sass, and CoffeeScript."
6
+ gem.homepage = "https://github.com/audiosocket/appetizer-ui"
7
+
8
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
9
+ gem.files = `git ls-files`.split("\n")
10
+ gem.test_files = `git ls-files -- test/*`.split("\n")
11
+ gem.name = "appetizer-ui"
12
+ gem.require_paths = ["lib"]
13
+ gem.version = "0.0.0"
14
+
15
+ gem.required_ruby_version = ">= 1.9.2"
16
+
17
+ gem.add_dependency "appetizer", "~> 0.0"
18
+ gem.add_dependency "coffee-script", "~> 2.2"
19
+ gem.add_dependency "eco", "~> 1.0"
20
+ gem.add_dependency "rack-ssl", "~> 1.3"
21
+ gem.add_dependency "sass", "~> 3.1"
22
+ gem.add_dependency "sinatra", "~> 1.3"
23
+ gem.add_dependency "sprockets", "~> 2.1"
24
+ gem.add_dependency "uglifier", "~> 1.0"
25
+ gem.add_dependency "yajl-ruby", "~> 1.0"
26
+ gem.add_dependency "yui-compressor", "~> 0.9"
27
+ end
@@ -0,0 +1,83 @@
1
+ require "appetizer/init"
2
+ require "appetizer/ui/assets"
3
+ require "sass"
4
+ require "securerandom"
5
+ require "sinatra/base"
6
+ require "yajl"
7
+
8
+ module Appetizer
9
+ module UI
10
+ def self.registered app
11
+
12
+ # Make sure that exception handling works the same in
13
+ # development and production.
14
+
15
+ app.set :show_exceptions, false
16
+
17
+ # All production apps better be using SSL.
18
+
19
+ app.configure :production do
20
+ require "rack/ssl"
21
+ app.use Rack::SSL
22
+ end
23
+
24
+ # This stack in primarily intended for deployment on Heroku, so
25
+ # only bother to log requests in development mode. Heroku's
26
+ # logging is more than enough in production.
27
+
28
+ app.configure :development do
29
+ app.use Rack::CommonLogger, App.log
30
+ end
31
+
32
+ # Build CSS under tmp, not in the project root.
33
+
34
+ app.set :scss, cache_location: "tmp/sass-cache", style: :compact
35
+
36
+ # Set up cookie sessions and authenticity token checking. Add
37
+ # some basic defaults, but allow them to be overridden.
38
+
39
+ app.use Rack::Session::Cookie,
40
+ key: (ENV["APPETIZER_COOKIE_NAME"] || "app-session"),
41
+ secret: (ENV["APPETIZER_SESSION_SECRET"] || "app-session-secret")
42
+
43
+ app.use Rack::Protection::AuthenticityToken
44
+
45
+ app.helpers do
46
+
47
+ # JSONify `thing` and respond with a `201`.
48
+
49
+ def created thing
50
+ halt 201, json(thing)
51
+ end
52
+
53
+ # The current CSRF token.
54
+
55
+ def csrf
56
+ session[:csrf] ||= SecureRandom.hex 32
57
+ end
58
+
59
+ # Set a `:json` content-type and run `thing` through the Yajl
60
+ # JSON encoder.
61
+
62
+ def json thing
63
+ content_type :json, charset: "utf-8"
64
+ jsonify thing
65
+ end
66
+
67
+ # Encode `thing` as JSON.
68
+
69
+ def jsonify thing
70
+ Yajl::Encoder.encode thing
71
+ end
72
+
73
+ # The asset manifest.
74
+
75
+ def manifest
76
+ Appetizer::UI::Assets.manifest
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ Sinatra.register Appetizer::UI
@@ -0,0 +1,9 @@
1
+ #= require json2
2
+ #= require jquery
3
+ #= require underscore
4
+ #= require underscore.string
5
+ #= require backbone
6
+
7
+ #= require appetizer/core
8
+ #= require appetizer/model
9
+ #= require appetizer/view
@@ -0,0 +1 @@
1
+ window.Appetizer ||= {}
@@ -0,0 +1 @@
1
+ class Appetizer.Model extends Backbone.Model
@@ -0,0 +1,138 @@
1
+ # Shockingly enough, a superclass for views. Provides hooks and
2
+ # abstractions for incoming and outgoing bindings, parent and child
3
+ # views, default template rendering, async actions before show, and
4
+ # show()/hide()/makeVisible() helpers on top of render() and remove().
5
+
6
+ class Appetizer.View extends Backbone.View
7
+
8
+ # Subclasses must always call `super` in their initializers:
9
+ # Important binding, parent, and child relationships get created.
10
+
11
+ initialize: (options) ->
12
+ @bindings = []
13
+ @children = []
14
+ @parent = options.parent if options?.parent?
15
+
16
+ @bind "ancestor:shown", ->
17
+ child.trigger "ancestor:shown" for child in @children
18
+ @shown = true
19
+
20
+ # true if the element or its ancestor have already been shown.
21
+ # Can be used at rendering time to execute operations that
22
+ # need the element to be hooked-up in the DOM, such as computing
23
+ # its height.
24
+
25
+ shown: false
26
+
27
+ # Add a child `view`. Its `parent` will be set to `this`, and it
28
+ # will be dismissed when this view is dismissed.
29
+
30
+ addChild: (view) ->
31
+ view.parent = this
32
+ @children.push view
33
+ this
34
+
35
+ # Hook to allow async processes to occur before and after showing
36
+ # the view. A bound callback `fn` is passed to this method by
37
+ # `show`, and should be called when the view is ready to be shown.
38
+
39
+ aroundShow: (fn) -> fn()
40
+
41
+ # Bind `fn` to an `event` on `src`, remembering that we've bound it
42
+ # for future unbinding. Use this instead of calling `bind` directly
43
+ # on other sources. Returns `this`.
44
+
45
+ bindTo: (src, event, fn) ->
46
+ src.bind event, fn, this
47
+ @bindings.push evt: event, fn: fn, src: src
48
+ this
49
+
50
+ # Create and return a new instance of `kind`, passing along
51
+ # `options` to the constructor. Add it as a child view.
52
+
53
+ createChild: (kind, options) ->
54
+ child = new kind options
55
+ @addChild child
56
+ child
57
+
58
+ # Indicate that this view is no longer necessary. Optionally takes a
59
+ # DOM event `e` and calls `preventDefault` on it to make wiring
60
+ # easier. Unbinds all incoming and outgoing events, calls `dismiss`
61
+ # on all children, and removes this view from any parent it might
62
+ # have. Returns `this`.
63
+
64
+ hide: (e) ->
65
+ e.preventDefault() if e?.preventDefault?
66
+
67
+ @trigger "hiding"
68
+ @remove()
69
+
70
+ @trigger "hidden"
71
+ @unbind()
72
+
73
+ _(@children).chain().clone().each (c) -> c.hide() if c.hide?
74
+ @parent.removeChild this if @parent?.removeChild
75
+
76
+ this
77
+
78
+ # Hook to provide actual DOM insertion/manipulation for the
79
+ # view. The default implementation logs an error message to the
80
+ # console.
81
+
82
+ makeVisible: ->
83
+ console.log "Can't make #{this.constructor.name} visible."
84
+
85
+ # Remove `view` from this view's list of children. Returns `this`.
86
+
87
+ removeChild: (view) ->
88
+ @children.splice _.indexOf(@children, view), 1
89
+ this
90
+
91
+ # Render the contents of the view. Updates the view element's
92
+ # contents, but doesn't do any other manipulation. Uses a JST based
93
+ # on either the view class' `template` default value or a `template`
94
+ # key passed in to the constructor. Triggers a "rendered" event
95
+ # after the element's contents are in place. Returns `this`.
96
+ #
97
+ # Assumes that views are under a "client/" prefix and that their
98
+ # template functions are available on a "JST" global.
99
+
100
+ render: =>
101
+ template = @options.template || @template || "appetizer/missing"
102
+ renderer = JST["client/#{template}"] or JST["client/appetizer/missing"]
103
+
104
+ @trigger "rendering"
105
+ $(@el).html renderer this
106
+ @trigger "rendered"
107
+
108
+ this
109
+
110
+ # Display this view. Possibly asynchronous: Passes a bound callback
111
+ # to `beforeShow` that will call `makeVisible` when the view is
112
+ # ready for display. The default implementation of `makeVisible`
113
+ # calls the callback immediately. Returns `this`.
114
+
115
+ show: ->
116
+ @aroundShow =>
117
+ @trigger "showing"
118
+ @makeVisible()
119
+ @trigger "shown"
120
+
121
+ @shown = true
122
+ child.trigger "ancestor:shown" for child in @children
123
+
124
+ this
125
+
126
+ # Unbind any listeners who have bound themselves to us, and unbind
127
+ # any listeners we've bound to others. Returns `this`.
128
+
129
+ unbind: ->
130
+ super # from Backbone.Events
131
+ b.src.unbind b.evt, b.fn for b in @bindings
132
+ this
133
+
134
+ # Unbind all events we may have bound on `src`. Returns `this`.
135
+
136
+ unbindFrom: (src) ->
137
+ b.src.unbind b.evt, b.fn for b in @bindings when b.src is src
138
+ this
@@ -0,0 +1,46 @@
1
+ # IE XDomainRequest support. Oh how I hate it.
2
+
3
+ statii =
4
+ 200: "OK"
5
+ 201: "CREATED"
6
+ 202: "ACCEPTED"
7
+ 204: "NO CONTENT"
8
+ 401: "UNAUTHORIZED"
9
+ 403: "FORBIDDEN"
10
+ 404: "NOT FOUND"
11
+ 409: "CONFLICT"
12
+ 422: "PRECONDITION FAILED"
13
+ 500: "INTERNAL SERVER ERROR"
14
+
15
+ Appetizer.transportXDR = (settings, original, xhr) ->
16
+ xdr = new XDomainRequest
17
+ sep = if settings.url.indexOf("?") is -1 then "?" else "&"
18
+ url = [settings.url, "xdr"].join sep
19
+
20
+ xdr.open "POST", url
21
+
22
+ abort: ->
23
+ xdr.abort()
24
+
25
+ send: (headers, complete) ->
26
+ xdr.onerror = ->
27
+ console.log "FIX: xdr onerror"
28
+
29
+ xdr.onload = ->
30
+ [status, headers, body] = $.parseJSON xdr.responseText
31
+
32
+ description = statii[status] or "UNKNOWN"
33
+ responses = text: body
34
+
35
+ complete status, description, responses, headers
36
+
37
+ xdr.onprogress = ->
38
+
39
+ # HACK: I wasn't able to get multiple requests to work (only
40
+ # the first one would ever trigger `onload`) until I assigned
41
+ # this empty handler to `onprogress`. What the actual fuck.
42
+
43
+ xdr.ontimeout = ->
44
+ console.log "FIX: xdr ontimeout"
45
+
46
+ xdr.send JSON.stringify [settings.type, headers, settings.data]
@@ -0,0 +1,3 @@
1
+ <div style="background-color:pink;border:1px solid red">
2
+ Missing template for <%= @constructor.name %>.
3
+ </div>
@@ -0,0 +1,91 @@
1
+ require "coffee-script"
2
+ require "eco"
3
+ require "fileutils"
4
+ require "sass"
5
+ require "securerandom"
6
+ require "sinatra/base"
7
+ require "sprockets"
8
+ require "uglifier"
9
+ require "yui/compressor"
10
+
11
+ module App
12
+ def self.assets
13
+ @sprockets ||= Sprockets::Environment.new.tap do |s|
14
+ if App.production? || ENV["APPETIZER_MINIFY_ASSETS"]
15
+ s.register_bundle_processor "application/javascript", :uglifier do |ctx, data|
16
+ Uglifier.compile data
17
+ end
18
+
19
+ s.register_bundle_processor "text/css", :yui do |ctx, data|
20
+ YUI::CssCompressor.new.compress data
21
+ end
22
+ end
23
+
24
+ # NOTE: Seems like Sprockets' built-in FileStore is kinda busted
25
+ # in the way it creates directories or processes key names (or I
26
+ # don't understand it yet), so we're manually creating the
27
+ # over-nested directory for the moment.
28
+
29
+ unless App.production?
30
+ FileUtils.mkdir_p "tmp/sprockets/sprockets"
31
+ s.cache = Sprockets::Cache::FileStore.new "tmp/sprockets"
32
+ end
33
+
34
+ %w(css img js views).each do |d|
35
+ s.append_path "./app/#{d}"
36
+ s.append_path "./vendor/#{d}"
37
+ s.append_path File.expand_path("../app/#{d}", __FILE__)
38
+ s.append_path File.expand_path("../vendor/#{d}", __FILE__)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ module Appetizer
45
+ module UI
46
+ module Assets
47
+ def self.manifest
48
+ return @manifest if defined? @manifest
49
+
50
+ @manifest = Hash.new { |h, k| k }
51
+
52
+ if File.file? file = "public/assets/manifest.yml"
53
+ require "yaml"
54
+ @manifest.merge! YAML.load File.read file
55
+ end
56
+
57
+ @manifest
58
+ end
59
+
60
+ def self.registered app
61
+ app.helpers do
62
+ def asset name
63
+ if App.production?
64
+ return cdnify "/assets/#{Appetizer::UI::Assets.manifest[name]}"
65
+ end
66
+
67
+ cdnify "/assets/#{App.assets[name].logical_path}"
68
+ end
69
+
70
+ def assets *names
71
+ names.flat_map do |name|
72
+ next asset name if App.production?
73
+
74
+ asset = App.assets[name]
75
+
76
+ [asset.dependencies, asset].flatten.map do |dep|
77
+ "/assets/#{dep.logical_path}?body=true&buster=#{SecureRandom.hex 10}"
78
+ end
79
+ end
80
+ end
81
+
82
+ def cdnify path
83
+ File.join [ENV["APPETIZER_CDN_URL"], path].compact
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ Sinatra.register Appetizer::UI::Assets