clearwater 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/clearwater.rb +7 -0
- data/lib/clearwater/version.rb +3 -0
- data/opal/clearwater.rb +4 -0
- data/opal/clearwater/api_client.rb +90 -0
- data/opal/clearwater/application.rb +128 -0
- data/opal/clearwater/application_registry.rb +23 -0
- data/opal/clearwater/cached_render.rb +15 -0
- data/opal/clearwater/cgi.rb +14 -0
- data/opal/clearwater/component.rb +205 -0
- data/opal/clearwater/link.rb +122 -0
- data/opal/clearwater/model.rb +33 -0
- data/opal/clearwater/router.rb +98 -0
- data/opal/clearwater/router/route.rb +43 -0
- data/opal/clearwater/router/route_collection.rb +52 -0
- data/opal/clearwater/store.rb +99 -0
- data/opal/clearwater/virtual_dom.rb +108 -0
- data/opal/clearwater/virtual_dom/js/virtual_dom.js +1663 -0
- metadata +174 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'clearwater/component'
|
2
|
+
require 'browser/history'
|
3
|
+
|
4
|
+
# TODO: Remove this once opal-browser supports coordinates natively
|
5
|
+
# for touch events.
|
6
|
+
module Browser
|
7
|
+
class Event
|
8
|
+
class Touch
|
9
|
+
def x
|
10
|
+
`#@native.pageX`
|
11
|
+
end
|
12
|
+
|
13
|
+
def y
|
14
|
+
`#@native.pageY`
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Link
|
21
|
+
include Clearwater::Component
|
22
|
+
|
23
|
+
attr_reader :attributes, :content
|
24
|
+
|
25
|
+
def initialize(attributes={}, content=nil)
|
26
|
+
# Augment the onclick passed in by the user, don't replace it.
|
27
|
+
@onclick = attributes.dup.delete(:onclick)
|
28
|
+
@attributes = attributes.merge(
|
29
|
+
onclick: method(:handle_click),
|
30
|
+
ontouchstart: method(:handle_touch),
|
31
|
+
key: attributes[:href]
|
32
|
+
)
|
33
|
+
|
34
|
+
check_active attributes[:href]
|
35
|
+
|
36
|
+
@content = content
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_click event
|
40
|
+
if @onclick
|
41
|
+
@onclick.call event
|
42
|
+
end
|
43
|
+
|
44
|
+
if touch?
|
45
|
+
event.prevent
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
if event.prevented?
|
50
|
+
warn "You are preventing the default behavior of a `Link` component. " +
|
51
|
+
"In this case, you could just use an `a` element."
|
52
|
+
else
|
53
|
+
navigate event
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_touch event
|
58
|
+
# All links will treat this as touch because this is a touch device
|
59
|
+
@@touch = true
|
60
|
+
moved = false
|
61
|
+
x_start = event.x
|
62
|
+
y_start = event.y
|
63
|
+
|
64
|
+
touch_move_handler = proc do |event|
|
65
|
+
x_now = event.x
|
66
|
+
y_now = event.y
|
67
|
+
|
68
|
+
# Count this gesture as a non-click if user moves over 30px
|
69
|
+
if ((x_now - x_start) ** 2 + (y_now - y_start) ** 2) ** 0.5 > 30
|
70
|
+
moved = true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
touch_end_handler = proc do
|
75
|
+
unless moved
|
76
|
+
@onclick.call event if @onclick
|
77
|
+
navigate event
|
78
|
+
end
|
79
|
+
|
80
|
+
$document.off 'touchmove', &touch_move_handler
|
81
|
+
$document.off 'touchend', &touch_end_handler
|
82
|
+
end
|
83
|
+
|
84
|
+
$document.on 'touchmove', &touch_move_handler
|
85
|
+
$document.on 'touchend', &touch_end_handler
|
86
|
+
end
|
87
|
+
|
88
|
+
def navigate event
|
89
|
+
unless event.meta? || event.shift? || event.ctrl? || event.alt?
|
90
|
+
event.prevent
|
91
|
+
if href != $window.location.path
|
92
|
+
$window.history.push href
|
93
|
+
call
|
94
|
+
$window.scroll.to x: 0, y: 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def href
|
100
|
+
attributes[:href]
|
101
|
+
end
|
102
|
+
|
103
|
+
def render
|
104
|
+
a(attributes, content)
|
105
|
+
end
|
106
|
+
|
107
|
+
def touch?
|
108
|
+
!!@@touch
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_active href
|
112
|
+
if $window.location.path == href
|
113
|
+
class_name = (
|
114
|
+
@attributes.delete(:class_name) ||
|
115
|
+
@attributes.delete(:class) ||
|
116
|
+
@attributes.delete(:className)
|
117
|
+
).to_s
|
118
|
+
|
119
|
+
@attributes[:className] = "#{class_name} active"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "clearwater/binding"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Model
|
5
|
+
def initialize attributes={}
|
6
|
+
@_bindings = Hash.new { |h,k| h[k] = [] }
|
7
|
+
self.class.attributes.each do |attr|
|
8
|
+
public_send "#{attr}=", attributes[attr]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_binding attribute, &block
|
13
|
+
binding = Binding.new(self, attribute, &block)
|
14
|
+
@_bindings[attribute].delete_if(&:dead?)
|
15
|
+
@_bindings[attribute] << binding
|
16
|
+
binding
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.attributes *args
|
20
|
+
@attributes ||= []
|
21
|
+
args.each do |attr|
|
22
|
+
attr_reader attr
|
23
|
+
|
24
|
+
define_method "#{attr}=" do |value|
|
25
|
+
instance_variable_set "@#{attr}", value
|
26
|
+
@_bindings[attr].each(&:call)
|
27
|
+
@_bindings[attr].delete_if(&:dead?)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@attributes.concat args
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "clearwater/router/route_collection"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
attr_reader :window, :location, :history
|
6
|
+
attr_accessor :application
|
7
|
+
|
8
|
+
def initialize options={}, &block
|
9
|
+
@window = options.fetch(:window) { Native(`window`) }
|
10
|
+
@location = options.fetch(:location) { window[:location] }
|
11
|
+
@history = options.fetch(:history) { window[:history] }
|
12
|
+
@routes = RouteCollection.new(self)
|
13
|
+
@application = options[:application]
|
14
|
+
|
15
|
+
add_routes(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_routes &block
|
19
|
+
@routes.instance_exec(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def routes_for_path path
|
23
|
+
parts = path.split("/").reject(&:empty?)
|
24
|
+
@routes[parts]
|
25
|
+
end
|
26
|
+
|
27
|
+
def canonical_path_for_path path
|
28
|
+
routes_for_path(path).map { |r| "/#{r.key}" }.join
|
29
|
+
end
|
30
|
+
|
31
|
+
def targets_for_path path
|
32
|
+
routes_for_path(path).map(&:target)
|
33
|
+
end
|
34
|
+
|
35
|
+
def params_for_path path
|
36
|
+
path_parts = path.split("/").reject(&:empty?)
|
37
|
+
canonical_parts = canonical_path_for_path(path).split("/").reject(&:empty?)
|
38
|
+
params = {}
|
39
|
+
canonical_parts.each_with_object(params)
|
40
|
+
.each_with_index { |(part, params), index|
|
41
|
+
if part.start_with? ":"
|
42
|
+
param = part[1..-1].to_sym
|
43
|
+
params[param] = path_parts[index]
|
44
|
+
end
|
45
|
+
}
|
46
|
+
params
|
47
|
+
end
|
48
|
+
|
49
|
+
def canonical_path
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_path
|
53
|
+
location[:pathname]
|
54
|
+
end
|
55
|
+
|
56
|
+
def navigate_to path
|
57
|
+
push_state path
|
58
|
+
set_outlets
|
59
|
+
render_application
|
60
|
+
end
|
61
|
+
|
62
|
+
def navigate_to_remote path
|
63
|
+
location[:href] = path
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_url
|
67
|
+
location[:href]
|
68
|
+
end
|
69
|
+
|
70
|
+
def back
|
71
|
+
history.back
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_outlets targets=targets_for_path(current_path)
|
75
|
+
if targets.any?
|
76
|
+
(targets.count).times do |index|
|
77
|
+
targets[index].outlet = targets[index + 1]
|
78
|
+
end
|
79
|
+
|
80
|
+
application && application.component.outlet = targets.first
|
81
|
+
else
|
82
|
+
application && application.component.outlet = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def push_state path
|
89
|
+
history.pushState({}, nil, path)
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_application
|
93
|
+
if application && application.component
|
94
|
+
application.component.call
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "clearwater/router/route_collection"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
class Route
|
6
|
+
attr_reader :target, :key, :parent
|
7
|
+
|
8
|
+
def initialize options={}
|
9
|
+
@key = options.fetch(:key)
|
10
|
+
@target = options.fetch(:target)
|
11
|
+
@parent = options.fetch(:parent)
|
12
|
+
end
|
13
|
+
|
14
|
+
def route *args, &block
|
15
|
+
nested_routes.route *args, &block
|
16
|
+
end
|
17
|
+
|
18
|
+
def canonical_path
|
19
|
+
@canonical_path ||= "#{parent.canonical_path}/#{key}".gsub("//", "/")
|
20
|
+
end
|
21
|
+
|
22
|
+
def match key, other_parts=[]
|
23
|
+
if key && (key == self.key || param_key?)
|
24
|
+
if Array(other_parts).any?
|
25
|
+
[self, nested_routes[other_parts]]
|
26
|
+
else
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def param_key?
|
35
|
+
@param_key ||= key.start_with? ":"
|
36
|
+
end
|
37
|
+
|
38
|
+
def nested_routes
|
39
|
+
@nested_routes ||= RouteCollection.new(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "clearwater/router/route"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
class RouteCollection
|
6
|
+
attr_reader :router
|
7
|
+
|
8
|
+
def initialize parent
|
9
|
+
@routes = []
|
10
|
+
@parent = parent
|
11
|
+
@router = loop do
|
12
|
+
break parent if parent.is_a? Router
|
13
|
+
parent = parent.parent
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def route route_options, &block
|
18
|
+
route_key = route_options.keys.first.to_s
|
19
|
+
target = route_options.delete(route_key)
|
20
|
+
target.router = router
|
21
|
+
options = {
|
22
|
+
key: route_key,
|
23
|
+
target: target,
|
24
|
+
parent: parent,
|
25
|
+
}.merge(route_options)
|
26
|
+
route = Route.new(options)
|
27
|
+
route.instance_exec(&block) if block_given?
|
28
|
+
@routes << route
|
29
|
+
end
|
30
|
+
|
31
|
+
def namespace path
|
32
|
+
@namespace = path
|
33
|
+
end
|
34
|
+
|
35
|
+
def [] route_names
|
36
|
+
if route_names.any? && route_names.first == @namespace
|
37
|
+
route_names = route_names[1..-1]
|
38
|
+
end
|
39
|
+
routes = @routes.map { |r|
|
40
|
+
r.match route_names.first, route_names[1..-1]
|
41
|
+
}
|
42
|
+
routes.compact!
|
43
|
+
routes.flatten!
|
44
|
+
routes
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :parent
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Store
|
5
|
+
attr_reader :mapping, :identity_map
|
6
|
+
|
7
|
+
def initialize options={}
|
8
|
+
@url = options.fetch(:url) { "/api/:model/:id" }
|
9
|
+
@mapping = Mapping.new(options.fetch(:mapping) { {} })
|
10
|
+
@protocol = options.fetch(:protocol) { HTTP }
|
11
|
+
@identity_map = IdentityMap.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def all klass
|
15
|
+
deserialized = api(:get, url_for(klass)).body
|
16
|
+
models = JSON.parse(deserialized).map do |attributes|
|
17
|
+
deserialize(klass, attributes)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def find klass, id
|
22
|
+
identity_map.fetch(klass, id) do
|
23
|
+
serialized = api(:get, url_for(klass, id)).body
|
24
|
+
identity_map[klass][id] = deserialize(klass, JSON.parse(serialized))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def save model
|
29
|
+
method = persisted?(model) ? :patch : :post
|
30
|
+
response = api method, url_for_model(model)
|
31
|
+
if method == :post && response.ok
|
32
|
+
attributes = JSON.parse(response.body)
|
33
|
+
model.instance_variable_set :@id, attributes[:id]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete model
|
38
|
+
api :delete, url_for_model(model)
|
39
|
+
end
|
40
|
+
|
41
|
+
def persisted? model
|
42
|
+
!!model.id
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def api method, url
|
48
|
+
@protocol.public_send method, url
|
49
|
+
end
|
50
|
+
|
51
|
+
def url_for klass, id=nil
|
52
|
+
@url.gsub(":model", mapping[klass])
|
53
|
+
.gsub(":id", id.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
def url_for_model model
|
57
|
+
url_for(model.class, model.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def url_for_class klass, id
|
61
|
+
url_for(klass, nil)
|
62
|
+
end
|
63
|
+
|
64
|
+
def deserialize klass, attributes
|
65
|
+
model = klass.allocate
|
66
|
+
attributes.each do |attr, value|
|
67
|
+
model.instance_variable_set "@#{attr}", value
|
68
|
+
end
|
69
|
+
|
70
|
+
model
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Mapping
|
75
|
+
def initialize mappings={}
|
76
|
+
@custom_mappings = mappings
|
77
|
+
end
|
78
|
+
|
79
|
+
def [] klass
|
80
|
+
@custom_mappings.fetch(klass) { |*args|
|
81
|
+
klass.name.downcase.gsub("::", "/") + "s"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class IdentityMap
|
87
|
+
def initialize
|
88
|
+
@map = Hash.new { |h, k| h[k] = {} }
|
89
|
+
end
|
90
|
+
|
91
|
+
def [] key
|
92
|
+
@map[key]
|
93
|
+
end
|
94
|
+
|
95
|
+
def fetch(klass, id, &block)
|
96
|
+
self[klass].fetch(id, &block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|