glimmer-dsl-web 0.2.6 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ require 'glimmer/data_binding/observer'
2
+
3
+ require_relative '../models/todo'
4
+
5
+ class TodoPresenter
6
+ FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/
7
+
8
+ attr_accessor :todos, :can_clear_completed, :active_todo_count
9
+ attr_reader :new_todo, :filter
10
+
11
+ def initialize
12
+ @todos = Todo.all.clone
13
+ @new_todo = Todo.new(task: '')
14
+ @filter = :all
15
+ refresh_todo_stats
16
+ end
17
+
18
+ def create_todo(todo = nil)
19
+ todo ||= new_todo.clone
20
+ Todo.all.prepend(todo)
21
+ observers_for_todo_stats[todo.object_id] = todo_stat_observer.observe(todo, :completed) unless observers_for_todo_stats.has_key?(todo.object_id)
22
+ refresh_todos_with_filter
23
+ refresh_todo_stats
24
+ new_todo.task = ''
25
+ end
26
+
27
+ def refresh_todos_with_filter
28
+ self.todos = Todo.send(filter).clone
29
+ end
30
+
31
+ def filter=(filter)
32
+ return if filter == @filter
33
+ @filter = filter
34
+ refresh_todos_with_filter
35
+ end
36
+
37
+ def destroy(todo)
38
+ delete(todo)
39
+ refresh_todos_with_filter
40
+ refresh_todo_stats
41
+ end
42
+
43
+ def clear_completed
44
+ Todo.completed.each { |todo| delete(todo) }
45
+ refresh_todos_with_filter
46
+ refresh_todo_stats
47
+ end
48
+
49
+ def toggle_all_completed
50
+ target_completed_value = Todo.active.any?
51
+ todos_to_update = target_completed_value ? Todo.active : Todo.completed
52
+ todos_to_update.each { |todo| todo.completed = target_completed_value }
53
+ end
54
+
55
+ def setup_filter_routes
56
+ @filter_router_function = -> (event) { apply_route_filter }
57
+ $$.addEventListener('popstate', &@filter_router_function)
58
+ apply_route_filter
59
+ end
60
+
61
+ def apply_route_filter
62
+ route_filter_match = $$.document.location.href.to_s.match(FILTER_ROUTE_REGEXP)
63
+ return if route_filter_match.nil?
64
+ route_filter = route_filter_match[1]
65
+ route_filter = 'all' if route_filter == ''
66
+ self.filter = route_filter
67
+ end
68
+
69
+ def unsetup_filter_routes
70
+ $$.removeEventListener('popstate', &@filter_router_function)
71
+ @filter_router_function = nil
72
+ end
73
+
74
+ private
75
+
76
+ def delete(todo)
77
+ Todo.all.delete(todo)
78
+ observer_registration = observers_for_todo_stats.delete(todo.object_id)
79
+ observer_registration&.deregister
80
+ end
81
+
82
+ def observers_for_todo_stats
83
+ @observers_for_todo_stats = {}
84
+ end
85
+
86
+ def todo_stat_observer
87
+ @todo_stat_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats }
88
+ end
89
+
90
+ def refresh_todo_stats
91
+ refresh_can_clear_completed
92
+ refresh_active_todo_count
93
+ end
94
+
95
+ def refresh_can_clear_completed
96
+ self.can_clear_completed = Todo.completed.any?
97
+ end
98
+
99
+ def refresh_active_todo_count
100
+ self.active_todo_count = Todo.active.count
101
+ end
102
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'todo_input'
2
+
3
+ class EditTodoInput < TodoInput
4
+ option :presenter
5
+ option :todo
6
+
7
+ markup {
8
+ input(class: todo_input_class) { |edit_input|
9
+ style <= [ todo, :editing,
10
+ on_read: ->(editing) { editing ? '' : 'display: none;' },
11
+ after_read: ->(_) { edit_input.focus if todo.editing? }
12
+ ]
13
+
14
+ value <=> [todo, :task]
15
+
16
+ onkeyup do |event|
17
+ if event.key == 'Enter' || event.keyCode == "\r"
18
+ todo.save_editing
19
+ presenter.destroy(todo) if todo.task.strip.empty?
20
+ elsif event.key == 'Escape' || event.keyCode == 27
21
+ todo.cancel_editing
22
+ end
23
+ end
24
+
25
+ onblur do |event|
26
+ todo.save_editing
27
+ end
28
+
29
+ style {
30
+ todo_input_styles
31
+ }
32
+ }
33
+ }
34
+
35
+ def todo_input_class
36
+ 'edit-todo'
37
+ end
38
+
39
+ def todo_input_styles
40
+ super
41
+
42
+ rule("*:has(> .#{todo_input_class})") {
43
+ position 'relative'
44
+ }
45
+
46
+ rule(".#{todo_input_class}") {
47
+ position 'absolute'
48
+ display 'block'
49
+ width 'calc(100% - 43px)'
50
+ padding '12px 16px'
51
+ margin '0 0 0 43px'
52
+ top '0'
53
+ }
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'new_todo_input'
2
+
3
+ class NewTodoForm
4
+ include Glimmer::Web::Component
5
+
6
+ option :presenter
7
+
8
+ markup {
9
+ header(class: 'header') {
10
+ h1('todos')
11
+
12
+ new_todo_input(presenter: presenter)
13
+
14
+ style {
15
+ rule('.header h1') {
16
+ color '#b83f45'
17
+ font_size '80px'
18
+ font_weight '200'
19
+ position 'absolute'
20
+ text_align 'center'
21
+ _webkit_text_rendering 'optimizeLegibility'
22
+ _moz_text_rendering 'optimizeLegibility'
23
+ text_rendering 'optimizeLegibility'
24
+ top '-140px'
25
+ width '100%'
26
+ }
27
+ }
28
+ }
29
+ }
30
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'todo_input'
2
+
3
+ class NewTodoInput < TodoInput
4
+ option :presenter
5
+
6
+ markup {
7
+ input(class: todo_input_class, placeholder: "What needs to be done?", autofocus: "") {
8
+ value <=> [presenter.new_todo, :task]
9
+
10
+ onkeyup do |event|
11
+ presenter.create_todo if event.key == 'Enter' || event.keyCode == "\r"
12
+ end
13
+
14
+ style {
15
+ todo_input_styles
16
+ }
17
+ }
18
+ }
19
+
20
+ def todo_input_class
21
+ 'new-todo'
22
+ end
23
+
24
+ def todo_input_styles
25
+ super
26
+
27
+ rule(".#{todo_input_class}") {
28
+ padding '16px 16px 16px 60px'
29
+ height '65px'
30
+ border 'none'
31
+ background 'rgba(0, 0, 0, 0.003)'
32
+ box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
33
+ }
34
+
35
+ rule(".#{todo_input_class}::placeholder") {
36
+ font_style 'italic'
37
+ font_weight '400'
38
+ color 'rgba(0, 0, 0, 0.4)'
39
+ }
40
+ end
41
+ end
@@ -0,0 +1,123 @@
1
+ class TodoFilters
2
+ include Glimmer::Web::Component
3
+
4
+ option :presenter
5
+
6
+ markup {
7
+ footer(class: 'todo-filters') {
8
+ style <= [ Todo, :all,
9
+ on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
10
+ ]
11
+
12
+ span(class: 'todo-count') {
13
+ span('.strong') {
14
+ inner_text <= [presenter, :active_todo_count]
15
+ }
16
+ span {
17
+ " items left"
18
+ }
19
+ }
20
+
21
+ ul(class: 'filters') {
22
+ Todo::FILTERS.each do |filter|
23
+ li {
24
+ a(filter.to_s.capitalize, href: "#/#{filter unless filter == :all}") {
25
+ class_name <= [ presenter, :filter,
26
+ on_read: -> (presenter_filter) { presenter_filter == filter ? 'selected' : '' }
27
+ ]
28
+
29
+ onclick do |event|
30
+ presenter.filter = filter
31
+ end
32
+ }
33
+ }
34
+ end
35
+ }
36
+
37
+ button('Clear completed', class: 'clear-completed') {
38
+ style <= [ presenter, :can_clear_completed,
39
+ on_read: -> (can_clear_completed) { can_clear_completed ? '' : 'display: none;' },
40
+ ]
41
+
42
+ onclick do |event|
43
+ presenter.clear_completed
44
+ end
45
+ }
46
+
47
+ style {
48
+ rule('.todo-filters') {
49
+ border_top '1px solid #e6e6e6'
50
+ font_size '15px'
51
+ height '20px'
52
+ padding '10px 15px'
53
+ text_align 'center'
54
+ }
55
+
56
+ rule('.todo-filters:before') {
57
+ bottom '0'
58
+ box_shadow '0 1px 1px rgba(0,0,0,.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0,0,0,.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0,0,0,.2)'
59
+ content '""'
60
+ height '50px'
61
+ left '0'
62
+ overflow 'hidden'
63
+ position 'absolute'
64
+ right '0'
65
+ }
66
+
67
+ rule('.todo-count') {
68
+ float 'left'
69
+ text_align 'left'
70
+ }
71
+
72
+ rule('.todo-count .strong') {
73
+ font_weight '300'
74
+ }
75
+
76
+ rule('.filters') {
77
+ left '0'
78
+ list_style 'none'
79
+ margin '0'
80
+ padding '0'
81
+ position 'absolute'
82
+ right '0'
83
+ }
84
+
85
+ rule('.filters li') {
86
+ display 'inline'
87
+ }
88
+
89
+ rule('.filters li a') {
90
+ border '1px solid transparent'
91
+ border_radius '3px'
92
+ color 'inherit'
93
+ margin '3px'
94
+ padding '3px 7px'
95
+ text_decoration 'none'
96
+ cursor 'pointer'
97
+ }
98
+
99
+ rule('.filters li a.selected') {
100
+ border_color '#ce4646'
101
+ }
102
+
103
+ rule('.clear-completed, html .clear-completed:active') {
104
+ cursor 'pointer'
105
+ float 'right'
106
+ line_height '19px'
107
+ position 'relative'
108
+ text_decoration 'none'
109
+ }
110
+
111
+ media('(max-width: 430px)') {
112
+ rule('.todo-filters') {
113
+ height '50px'
114
+ }
115
+
116
+ rule('.filters') {
117
+ bottom '10px'
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ end
@@ -0,0 +1,30 @@
1
+ # Superclass for NewTodoInput and EditTodoInput with common styles
2
+ class TodoInput
3
+ include Glimmer::Web::Component
4
+
5
+ def todo_input_class
6
+ 'todo-input'
7
+ end
8
+
9
+ def todo_input_styles
10
+ rule(".#{todo_input_class}") {
11
+ position 'relative'
12
+ margin '0'
13
+ width '100%'
14
+ font_size '24px'
15
+ font_family 'inherit'
16
+ font_weight 'inherit'
17
+ line_height '1.4em'
18
+ color 'inherit'
19
+ padding '6px'
20
+ border '1px solid #999'
21
+ box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
22
+ box_sizing 'border-box'
23
+ _webkit_font_smoothing 'antialiased'
24
+ }
25
+
26
+ rule(".#{todo_input_class}::selection") {
27
+ background 'red'
28
+ }
29
+ end
30
+ end
@@ -0,0 +1,88 @@
1
+ require_relative 'todo_list_item'
2
+
3
+ class TodoList
4
+ include Glimmer::Web::Component
5
+
6
+ option :presenter
7
+
8
+ markup {
9
+ main(class: 'main') {
10
+ style <= [ Todo, :all,
11
+ on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
12
+ ]
13
+
14
+ div(class: 'toggle-all-container') {
15
+ input(class: 'toggle-all', type: 'checkbox')
16
+
17
+ label('Mark all as complete', class: 'toggle-all-label', for: 'toggle-all') {
18
+ onclick do |event|
19
+ presenter.toggle_all_completed
20
+ end
21
+ }
22
+ }
23
+
24
+ ul(class: 'todo-list') {
25
+ content(presenter, :todos) {
26
+ presenter.todos.each do |todo|
27
+ todo_list_item(presenter:, todo:)
28
+ end
29
+ }
30
+ }
31
+
32
+ style {
33
+ todo_list_styles
34
+ }
35
+ }
36
+ }
37
+
38
+ def todo_list_styles
39
+ rule('.main') {
40
+ border_top '1px solid #e6e6e6'
41
+ position 'relative'
42
+ z_index '2'
43
+ }
44
+
45
+ rule('.toggle-all') {
46
+ border 'none'
47
+ bottom '100%'
48
+ height '1px'
49
+ opacity '0'
50
+ position 'absolute'
51
+ right '100%'
52
+ width '1px'
53
+ }
54
+
55
+ rule('.toggle-all+label') {
56
+ align_items 'center'
57
+ display 'flex'
58
+ font_size '0'
59
+ height '65px'
60
+ justify_content 'center'
61
+ left '0'
62
+ position 'absolute'
63
+ top '-65px'
64
+ width '45px'
65
+ }
66
+
67
+ rule('.toggle-all+label:before') {
68
+ color '#949494'
69
+ content '"❯"'
70
+ display 'inline-block'
71
+ font_size '22px'
72
+ padding '10px 27px'
73
+ _webkit_transform 'rotate(90deg)'
74
+ transform 'rotate(90deg)'
75
+ }
76
+
77
+ rule('.toggle-all:focus+label, .toggle:focus+label, :focus') {
78
+ box_shadow '0 0 2px 2px #cf7d7d'
79
+ outline '0'
80
+ }
81
+
82
+ rule('.todo-list') {
83
+ list_style 'none'
84
+ margin '0'
85
+ padding '0'
86
+ }
87
+ end
88
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'edit_todo_input'
2
+
3
+ class TodoListItem
4
+ include Glimmer::Web::Component
5
+
6
+ option :presenter
7
+ option :todo
8
+
9
+ markup {
10
+ li {
11
+ class_name <= [ todo, :completed,
12
+ on_read: -> (completed) { li_class_name(todo) }
13
+ ]
14
+ class_name <= [ todo, :editing,
15
+ on_read: -> (editing) { li_class_name(todo) }
16
+ ]
17
+
18
+ div(class: 'view') {
19
+ input(class: 'toggle', type: 'checkbox') {
20
+ checked <=> [ todo, :completed,
21
+ after_write: -> (_) { presenter.refresh_todos_with_filter if presenter.filter != :all }
22
+ ]
23
+ }
24
+
25
+ label {
26
+ inner_html <= [todo, :task]
27
+
28
+ ondblclick do |event|
29
+ todo.start_editing
30
+ end
31
+ }
32
+
33
+ button(class: 'destroy') {
34
+ onclick do |event|
35
+ presenter.destroy(todo)
36
+ end
37
+ }
38
+ }
39
+
40
+ edit_todo_input(presenter:, todo:)
41
+
42
+ if todo == presenter.todos.first
43
+ style {
44
+ todo_list_item_styles
45
+ }
46
+ end
47
+ }
48
+ }
49
+
50
+ def li_class_name(todo)
51
+ classes = []
52
+ classes << 'completed' if todo.completed?
53
+ classes << 'editing' if todo.editing?
54
+ classes.join(' ')
55
+ end
56
+
57
+ def todo_list_item_styles
58
+ rule('.todo-list li.completed label') {
59
+ color '#949494'
60
+ text_decoration 'line-through'
61
+ }
62
+
63
+ rule('.todo-list li') {
64
+ border_bottom '1px solid #ededed'
65
+ font_size '24px'
66
+ position 'relative'
67
+ }
68
+
69
+ rule('.todo-list li .toggle') {
70
+ _webkit_appearance 'none'
71
+ appearance 'none'
72
+ border 'none'
73
+ bottom '0'
74
+ height 'auto'
75
+ margin 'auto 0'
76
+ opacity '0'
77
+ position 'absolute'
78
+ text_align 'center'
79
+ top '0'
80
+ width '40px'
81
+ }
82
+
83
+ rule('.todo-list li label') {
84
+ color '#484848'
85
+ display 'block'
86
+ font_weight '400'
87
+ line_height '1.2'
88
+ min_height '40px'
89
+ padding '15px 15px 15px 60px'
90
+ transition 'color .4s'
91
+ word_break 'break-all'
92
+ }
93
+
94
+ rule('.todo-list li .toggle+label') {
95
+ background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E)'
96
+ background_position '0'
97
+ background_repeat 'no-repeat'
98
+ }
99
+
100
+ rule('.todo-list li.completed label') {
101
+ color '#949494'
102
+ text_decoration 'line-through'
103
+ }
104
+
105
+ rule('.todo-list li .toggle:checked+label') {
106
+ background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E)'
107
+ }
108
+
109
+ rule('.todo-list li.editing') {
110
+ border_bottom 'none'
111
+ padding '0'
112
+ }
113
+
114
+ rule('.todo-list li.editing input[type=checkbox], .todo-list li.editing label') {
115
+ opacity '0'
116
+ }
117
+
118
+ rule('.todo-list li .destroy') {
119
+ bottom '0'
120
+ color '#949494'
121
+ display 'none'
122
+ font_size '30px'
123
+ height '40px'
124
+ margin 'auto 0'
125
+ position 'absolute'
126
+ right '10px'
127
+ top '0'
128
+ transition 'color .2s ease-out'
129
+ width '40px'
130
+ }
131
+
132
+ rule('.todo-list li:focus .destroy, .todo-list li:hover .destroy') {
133
+ display 'block'
134
+ }
135
+
136
+ rule('.todo-list li .destroy:focus, .todo-list li .destroy:hover') {
137
+ color '#c18585'
138
+ }
139
+
140
+ rule('.todo-list li .destroy:after') {
141
+ content '"×"'
142
+ display 'block'
143
+ height '100%'
144
+ line_height '1.1'
145
+ }
146
+
147
+ media ('screen and (-webkit-min-device-pixel-ratio: 0)') {
148
+ rule('.todo-list li .toggle, .toggle-all') {
149
+ background 'none'
150
+ }
151
+
152
+ rule('.todo-list li .toggle') {
153
+ height '40px'
154
+ }
155
+ }
156
+ end
157
+ end
158
+
@@ -0,0 +1,45 @@
1
+ class TodoMvcFooter
2
+ include Glimmer::Web::Component
3
+
4
+ markup {
5
+ footer(class: 'info') {
6
+ p {
7
+ "Double-click to edit a todo"
8
+ }
9
+ p {
10
+ "Created by #{a('Andy Maleh', href: 'https://github.com/AndyObtiva')}"
11
+ }
12
+ p {
13
+ "Part of #{a('TodoMVC', href: 'http://todomvc.com')}"
14
+ }
15
+
16
+ style {
17
+ todo_mvc_styles
18
+ }
19
+ }
20
+ }
21
+
22
+ def todo_mvc_styles
23
+ rule('footer.info') {
24
+ margin '65px auto 0'
25
+ color '#4d4d4d'
26
+ font_size '11px'
27
+ text_shadow '0 1px 0 rgba(255, 255, 255, 0.5)'
28
+ text_align 'center'
29
+ }
30
+
31
+ rule('footer.info p') {
32
+ line_height '1'
33
+ }
34
+
35
+ rule('footer.info a') {
36
+ color 'inherit'
37
+ text_decoration 'none'
38
+ font_weight '400'
39
+ }
40
+
41
+ rule('footer.info a:hover') {
42
+ text_decoration 'underline'
43
+ }
44
+ end
45
+ end