lissio 0.1.0.beta1
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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +53 -0
- data/README.md +184 -0
- data/Rakefile +5 -0
- data/lib/lissio.rb +5 -0
- data/lib/lissio/server.rb +148 -0
- data/lissio.gemspec +27 -0
- data/opal/lissio.rb +26 -0
- data/opal/lissio/adapter.rb +45 -0
- data/opal/lissio/adapter/rest.rb +268 -0
- data/opal/lissio/adapter/storage.rb +167 -0
- data/opal/lissio/application.rb +66 -0
- data/opal/lissio/collection.rb +70 -0
- data/opal/lissio/component.rb +177 -0
- data/opal/lissio/component/alert.rb +110 -0
- data/opal/lissio/component/container.rb +56 -0
- data/opal/lissio/component/markdown.rb +332 -0
- data/opal/lissio/component/tooltip.rb +373 -0
- data/opal/lissio/model.rb +204 -0
- data/opal/lissio/router.rb +164 -0
- data/opal/lissio/version.rb +3 -0
- data/spec/route_spec.rb +65 -0
- data/spec/router_spec.rb +109 -0
- data/spec/spec_helper.rb +33 -0
- metadata +154 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'json'
|
12
|
+
require 'forwardable'
|
13
|
+
|
14
|
+
module Lissio
|
15
|
+
|
16
|
+
class Model
|
17
|
+
class Property
|
18
|
+
attr_reader :name, :as
|
19
|
+
|
20
|
+
def initialize(name, options)
|
21
|
+
@name = name
|
22
|
+
@default = options[:default]
|
23
|
+
@primary = options[:primary] || false
|
24
|
+
@as = options[:as]
|
25
|
+
end
|
26
|
+
|
27
|
+
def primary?
|
28
|
+
@primary
|
29
|
+
end
|
30
|
+
|
31
|
+
def default
|
32
|
+
if Proc === @default && @default.lambda?
|
33
|
+
@default.call
|
34
|
+
else
|
35
|
+
@default
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def new(data)
|
40
|
+
return default if data.nil?
|
41
|
+
return data if !@as || @as === data
|
42
|
+
|
43
|
+
if @as.ancestors.include?(Model)
|
44
|
+
if @as.primary.as === data
|
45
|
+
return data
|
46
|
+
else
|
47
|
+
return @as.new(*data)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
case
|
52
|
+
when @as == Boolean then !!data
|
53
|
+
when @as == Array then Array(data)
|
54
|
+
when @as == String then data.to_s
|
55
|
+
when @as == Symbol then data.to_sym
|
56
|
+
when @as == Integer then data.to_i
|
57
|
+
when @as == Float then data.to_f
|
58
|
+
when @as == Time then Time.parse(data)
|
59
|
+
when Proc === @as then @as.call(data)
|
60
|
+
else @as.new(*data)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def define(klass)
|
65
|
+
name = @name
|
66
|
+
as = @as
|
67
|
+
|
68
|
+
if Class === @as && @as.ancestors.include?(Model)
|
69
|
+
klass.define_method name do
|
70
|
+
if id = instance_variable_get("@#{name}")
|
71
|
+
as.fetch(id)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
klass.define_method "#{name}=" do |value|
|
76
|
+
if instance_variable_get("@#{name}") != value.id!
|
77
|
+
@changed << name
|
78
|
+
|
79
|
+
instance_variable_set "@#{name}", value.id!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
klass.define_method name do
|
84
|
+
instance_variable_get "@#{name}"
|
85
|
+
end
|
86
|
+
|
87
|
+
klass.define_method "#{name}=" do |value|
|
88
|
+
if instance_variable_get("@#{name}") != value
|
89
|
+
@changed << name
|
90
|
+
|
91
|
+
instance_variable_set "@#{name}", value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.inherited(klass)
|
99
|
+
klass.instance_eval {
|
100
|
+
@properties = {}
|
101
|
+
}
|
102
|
+
|
103
|
+
return if self == Model
|
104
|
+
|
105
|
+
klass.instance_eval {
|
106
|
+
def self.properties
|
107
|
+
superclass.properties.merge @properties
|
108
|
+
end
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.adapter(klass = nil, *args, &block)
|
113
|
+
if klass
|
114
|
+
@adapter.uninstall if @adapter
|
115
|
+
|
116
|
+
@adapter = klass.new(self, *args, &block)
|
117
|
+
@adapter.install
|
118
|
+
else
|
119
|
+
@adapter
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.for(klass, *args, &block)
|
124
|
+
Class.new(self) {
|
125
|
+
adapter(klass, *args, &block)
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.properties
|
130
|
+
@properties
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.property(name, options = {})
|
134
|
+
# @properties is accessed directly to allow proper inheritance
|
135
|
+
@properties[name] = Property.new(name, options).tap {|property|
|
136
|
+
property.define(self)
|
137
|
+
|
138
|
+
if property.primary?
|
139
|
+
@primary = property
|
140
|
+
end
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.primary
|
145
|
+
@primary
|
146
|
+
end
|
147
|
+
|
148
|
+
extend Forwardable
|
149
|
+
def_delegators :class, :adapter, :properties
|
150
|
+
|
151
|
+
attr_reader :fetched_with, :changed
|
152
|
+
|
153
|
+
def initialize(data = nil, *fetched_with)
|
154
|
+
@fetched_with = fetched_with
|
155
|
+
@changed = []
|
156
|
+
|
157
|
+
if data
|
158
|
+
properties.each {|name, property|
|
159
|
+
instance_variable_set "@#{name}", property.new(data[name])
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def changed?
|
165
|
+
not @changed.empty?
|
166
|
+
end
|
167
|
+
|
168
|
+
def id!
|
169
|
+
name, = properties.find {|_, property|
|
170
|
+
property.primary?
|
171
|
+
}
|
172
|
+
|
173
|
+
instance_variable_get "@#{name || :id}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_h
|
177
|
+
Hash[properties.map {|name, _|
|
178
|
+
[name, instance_variable_get("@#{name}")]
|
179
|
+
}]
|
180
|
+
end
|
181
|
+
|
182
|
+
def as_json
|
183
|
+
hash = to_h
|
184
|
+
hash[JSON.create_id] = self.class.name
|
185
|
+
|
186
|
+
hash
|
187
|
+
end
|
188
|
+
|
189
|
+
def to_json
|
190
|
+
as_json.to_json
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.json_create(data)
|
194
|
+
new(data)
|
195
|
+
end
|
196
|
+
|
197
|
+
def inspect
|
198
|
+
"#<#{self.class.name}: #{properties.map {|name, _|
|
199
|
+
"#{name}=#{instance_variable_get("@#{name}").inspect}"
|
200
|
+
}.join(' ')}>"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'browser/history'
|
12
|
+
|
13
|
+
module Lissio
|
14
|
+
|
15
|
+
class Router
|
16
|
+
attr_reader :routes, :options
|
17
|
+
|
18
|
+
def initialize(options = {}, &block)
|
19
|
+
@routes = []
|
20
|
+
@options = options
|
21
|
+
@location = $document.location
|
22
|
+
@initial = @location.uri
|
23
|
+
@history = $window.history
|
24
|
+
|
25
|
+
@change = if fragment?
|
26
|
+
$window.on 'hash:change' do
|
27
|
+
update
|
28
|
+
end
|
29
|
+
else
|
30
|
+
$window.on 'pop:state' do |e|
|
31
|
+
if @initial == @location.uri
|
32
|
+
@initial = nil
|
33
|
+
break
|
34
|
+
else
|
35
|
+
@initial = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
update
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if block.arity == 0
|
43
|
+
instance_exec(&block)
|
44
|
+
else
|
45
|
+
block.call(self)
|
46
|
+
end if block
|
47
|
+
end
|
48
|
+
|
49
|
+
def fragment?
|
50
|
+
@options[:fragment] != false || !`window.history.pushState`
|
51
|
+
end
|
52
|
+
|
53
|
+
def html5!
|
54
|
+
return unless fragment?
|
55
|
+
|
56
|
+
@options[:fragment] = false
|
57
|
+
|
58
|
+
@change.off
|
59
|
+
@change = $window.on 'pop:state' do
|
60
|
+
update
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def fragment!
|
65
|
+
return if fragment?
|
66
|
+
|
67
|
+
@options[:fragment] = true
|
68
|
+
|
69
|
+
@change.off
|
70
|
+
@change = $window.on 'hash:change' do
|
71
|
+
update
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def path
|
76
|
+
if fragment?
|
77
|
+
if @location.fragment.empty?
|
78
|
+
"/"
|
79
|
+
else
|
80
|
+
@location.fragment.sub(/^#*/, '')
|
81
|
+
end
|
82
|
+
else
|
83
|
+
@location.path
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def route(path, &block)
|
88
|
+
Route.new(path, &block).tap {|route|
|
89
|
+
@routes << route
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def update
|
94
|
+
match path
|
95
|
+
end
|
96
|
+
|
97
|
+
def navigate(path)
|
98
|
+
if path == self.path
|
99
|
+
return update
|
100
|
+
end
|
101
|
+
|
102
|
+
if fragment?
|
103
|
+
@location.fragment = "##{path}"
|
104
|
+
else
|
105
|
+
@history.push(path)
|
106
|
+
update
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def match(path)
|
112
|
+
@routes.find {|route|
|
113
|
+
route.match path
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
class Route
|
118
|
+
# Regexp for matching named params in path
|
119
|
+
NAME = /:(\w+)/
|
120
|
+
|
121
|
+
# Regexp for matching named splats in path
|
122
|
+
SPLAT = /\\\*(\w+)/
|
123
|
+
|
124
|
+
attr_reader :names
|
125
|
+
|
126
|
+
def initialize(pattern, &block)
|
127
|
+
@names = []
|
128
|
+
@block = block
|
129
|
+
|
130
|
+
pattern = Regexp.escape(pattern)
|
131
|
+
|
132
|
+
pattern.scan(NAME) {|name,|
|
133
|
+
@names << name
|
134
|
+
}
|
135
|
+
|
136
|
+
pattern.scan(SPLAT) {|name,|
|
137
|
+
@names << name
|
138
|
+
}
|
139
|
+
|
140
|
+
pattern = pattern.gsub(NAME, "([^\\/]+)").
|
141
|
+
gsub(SPLAT, "(.+?)")
|
142
|
+
|
143
|
+
@regexp = Regexp.new "^#{pattern}$"
|
144
|
+
end
|
145
|
+
|
146
|
+
def match(path)
|
147
|
+
if match = @regexp.match(path)
|
148
|
+
params = {}
|
149
|
+
|
150
|
+
@names.each_with_index {|name, index|
|
151
|
+
params[name] = match[index + 1]
|
152
|
+
}
|
153
|
+
|
154
|
+
@block.call params if @block
|
155
|
+
|
156
|
+
true
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
data/spec/route_spec.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lissio::Router::Route do
|
4
|
+
describe "#initialize" do
|
5
|
+
let(:route) { Lissio::Router::Route }
|
6
|
+
|
7
|
+
it "creates a regexp from the given pattern" do
|
8
|
+
route.new('foo').instance_variable_get(:@regexp).should eq(/^foo$/)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "escapes slashes in the pattern" do
|
12
|
+
route.new('/foo/bar/').instance_variable_get(:@regexp).should eq(/^\/foo\/bar\/$/)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "finds named params in pattern" do
|
16
|
+
r = route.new('/foo/:bar')
|
17
|
+
r.names.should eq(['bar'])
|
18
|
+
|
19
|
+
p = route.new('/:woosh/:kapow')
|
20
|
+
p.names.should eq(['woosh', 'kapow'])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "finds splatted params in pattern" do
|
24
|
+
route.new('/foo/*baz').names.should eq(['baz'])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "produces a regexp to match given pattern" do
|
28
|
+
route.new('/foo').instance_variable_get(:@regexp).match('/bar').should be_nil
|
29
|
+
route.new('/foo').instance_variable_get(:@regexp).match('/foo').should be_kind_of(MatchData)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#match" do
|
34
|
+
let(:route) { Lissio::Router::Route }
|
35
|
+
|
36
|
+
it "returns false for a non matching route" do
|
37
|
+
route.new('/foo').match('/a/b/c').should be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns true for a matching route" do
|
41
|
+
route.new('/foo').match('/foo').should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "calls block given to #initialize on matching a route" do
|
45
|
+
called = false
|
46
|
+
route.new('/foo') { called = true }.match('/foo')
|
47
|
+
called.should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "calls handler with an empty hash for a simple route" do
|
51
|
+
route.new('/foo') { |params| params.should eq({}) }.match('/foo')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns a hash of named params for matching route" do
|
55
|
+
route.new('/foo/:first') { |params|
|
56
|
+
params.should eq({'first' => '123' })
|
57
|
+
}.match('/foo/123')
|
58
|
+
|
59
|
+
route.new('/:first/:second') { |params|
|
60
|
+
params.should eq({ 'first' => 'woosh', 'second' => 'kapow' })
|
61
|
+
}.match('/woosh/kapow')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
data/spec/router_spec.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lissio::Router do
|
4
|
+
let(:router) { Lissio::Router.new }
|
5
|
+
|
6
|
+
def set_native_hash(hash)
|
7
|
+
`window.location.hash = #{hash}`
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#update" do
|
11
|
+
it "should update Router.path" do
|
12
|
+
set_native_hash '#/hello/world'
|
13
|
+
router.update
|
14
|
+
|
15
|
+
router.path.should eq('/hello/world')
|
16
|
+
end
|
17
|
+
|
18
|
+
it "calls #match with the new @path" do
|
19
|
+
set_native_hash '#/foo/bar'
|
20
|
+
called = nil
|
21
|
+
|
22
|
+
router.define_singleton_method(:match) { |m| called = m }
|
23
|
+
called.should be_nil
|
24
|
+
|
25
|
+
router.update
|
26
|
+
called.should eq("/foo/bar")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#route" do
|
31
|
+
it "should add a route" do
|
32
|
+
router.route('/users') {}
|
33
|
+
router.routes.size.should eq(1)
|
34
|
+
|
35
|
+
router.route('/hosts') {}
|
36
|
+
router.routes.size.should eq(2)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#match" do
|
41
|
+
it "returns nil when no routes on router" do
|
42
|
+
router.match('/foo').should be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it "returns a matching route for the path" do
|
46
|
+
a = router.route('/foo') {}
|
47
|
+
b = router.route('/bar') {}
|
48
|
+
|
49
|
+
router.match('/foo').should eq(a)
|
50
|
+
router.match('/bar').should eq(b)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns nil when there are no matching routes" do
|
54
|
+
router.route('/woosh') {}
|
55
|
+
router.route('/kapow') {}
|
56
|
+
|
57
|
+
router.match('/ping').should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "calls handler of matching route" do
|
61
|
+
out = []
|
62
|
+
router.route('/foo') { out << :foo }
|
63
|
+
router.route('/bar') { out << :bar }
|
64
|
+
|
65
|
+
router.match('/foo')
|
66
|
+
out.should eq([:foo])
|
67
|
+
|
68
|
+
router.match('/bar')
|
69
|
+
out.should eq([:foo, :bar])
|
70
|
+
|
71
|
+
router.match('/eek')
|
72
|
+
out.should eq([:foo, :bar])
|
73
|
+
end
|
74
|
+
|
75
|
+
it "works with / too" do
|
76
|
+
out = []
|
77
|
+
router.route('/') { out << :index }
|
78
|
+
|
79
|
+
$global.location.hash = ""
|
80
|
+
router.update
|
81
|
+
|
82
|
+
$global.location.hash = "#/"
|
83
|
+
router.update
|
84
|
+
|
85
|
+
out.should == [:index, :index]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#navigate" do
|
90
|
+
it "should update location.hash" do
|
91
|
+
router.navigate "foo"
|
92
|
+
$global.location.hash.should eq("#foo")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "triggers the route matchers" do
|
96
|
+
called = false
|
97
|
+
router.route("/foo") { called = true }
|
98
|
+
|
99
|
+
router.navigate("/bar")
|
100
|
+
router.update
|
101
|
+
called.should be_false
|
102
|
+
|
103
|
+
router.navigate("/foo")
|
104
|
+
router.update
|
105
|
+
called.should be_true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|