glimmer-dsl-web 0.2.6 → 0.2.8

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