appetizer-ui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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