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.
- 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
|