appetizer-ui 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +2 -0
- data/README.md +28 -0
- data/Rakefile +1 -0
- data/appetizer-ui.gemspec +27 -0
- data/lib/appetizer/ui.rb +83 -0
- data/lib/appetizer/ui/app/js/appetizer.coffee +9 -0
- data/lib/appetizer/ui/app/js/appetizer/core.coffee +1 -0
- data/lib/appetizer/ui/app/js/appetizer/model.coffee +1 -0
- data/lib/appetizer/ui/app/js/appetizer/view.coffee +138 -0
- data/lib/appetizer/ui/app/js/appetizer/xdr.coffee +46 -0
- data/lib/appetizer/ui/app/views/client/appetizer/missing.jst.eco +3 -0
- data/lib/appetizer/ui/assets.rb +91 -0
- data/lib/appetizer/ui/jasmine/css/jasmine.css +171 -0
- data/lib/appetizer/ui/jasmine/js/jasmine-html.js +190 -0
- data/lib/appetizer/ui/jasmine/js/jasmine-jquery-matchers.js +186 -0
- data/lib/appetizer/ui/jasmine/js/jasmine.js +2476 -0
- data/lib/appetizer/ui/jasmine/js/spec-runner.coffee +20 -0
- data/lib/appetizer/ui/jasmine/views/specs.erb +19 -0
- data/lib/appetizer/ui/rake.rb +33 -0
- data/lib/appetizer/ui/spec.rb +28 -0
- data/lib/appetizer/ui/vendor/js/backbone.js +1154 -0
- data/lib/appetizer/ui/vendor/js/jquery.js +9300 -0
- data/lib/appetizer/ui/vendor/js/json2.js +480 -0
- data/lib/appetizer/ui/vendor/js/underscore.js +839 -0
- data/lib/appetizer/ui/vendor/js/underscore.string.js +10 -0
- metadata +181 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/appetizer/ui.rb
ADDED
@@ -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 @@
|
|
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,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
|