fron-ui 1.0.0rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +38 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +8 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +105 -0
- data/Rakefile +37 -0
- data/Readme.md +4 -0
- data/db.json +192 -0
- data/fron-ui.gemspec +21 -0
- data/lib/fron-ui.rb +1 -0
- data/lib/fron_ui.rb +5 -0
- data/lib/fron_ui/version.rb +7 -0
- data/opal/fron-ui/base.rb +49 -0
- data/opal/fron-ui/behaviors/action.rb +40 -0
- data/opal/fron-ui/behaviors/actions.rb +40 -0
- data/opal/fron-ui/behaviors/confirmation.rb +23 -0
- data/opal/fron-ui/behaviors/dropdown.rb +27 -0
- data/opal/fron-ui/behaviors/file.rb +48 -0
- data/opal/fron-ui/behaviors/intendable_children.rb +76 -0
- data/opal/fron-ui/behaviors/keydown.rb +31 -0
- data/opal/fron-ui/behaviors/loop.rb +41 -0
- data/opal/fron-ui/behaviors/render.rb +30 -0
- data/opal/fron-ui/behaviors/rest.rb +121 -0
- data/opal/fron-ui/behaviors/selectable_children.rb +67 -0
- data/opal/fron-ui/behaviors/serialize.rb +32 -0
- data/opal/fron-ui/behaviors/shortcuts.rb +35 -0
- data/opal/fron-ui/behaviors/state.rb +56 -0
- data/opal/fron-ui/behaviors/transition.rb +63 -0
- data/opal/fron-ui/components/action.rb +18 -0
- data/opal/fron-ui/components/box.rb +17 -0
- data/opal/fron-ui/components/button.rb +61 -0
- data/opal/fron-ui/components/calendar.rb +129 -0
- data/opal/fron-ui/components/checkbox.rb +57 -0
- data/opal/fron-ui/components/chooser.rb +246 -0
- data/opal/fron-ui/components/color_panel.rb +235 -0
- data/opal/fron-ui/components/color_picker.rb +111 -0
- data/opal/fron-ui/components/container.rb +61 -0
- data/opal/fron-ui/components/date_picker.rb +141 -0
- data/opal/fron-ui/components/drag.rb +76 -0
- data/opal/fron-ui/components/dropdown.rb +72 -0
- data/opal/fron-ui/components/icon.rb +29 -0
- data/opal/fron-ui/components/image.rb +77 -0
- data/opal/fron-ui/components/input.rb +30 -0
- data/opal/fron-ui/components/label.rb +9 -0
- data/opal/fron-ui/components/list.rb +34 -0
- data/opal/fron-ui/components/loader.rb +63 -0
- data/opal/fron-ui/components/modal.rb +0 -0
- data/opal/fron-ui/components/notifications.rb +73 -0
- data/opal/fron-ui/components/number.rb +202 -0
- data/opal/fron-ui/components/progress.rb +52 -0
- data/opal/fron-ui/components/slider.rb +47 -0
- data/opal/fron-ui/components/tabs.rb +149 -0
- data/opal/fron-ui/components/textarea.rb +13 -0
- data/opal/fron-ui/components/time.rb +65 -0
- data/opal/fron-ui/components/title.rb +34 -0
- data/opal/fron-ui/examples/blog/index.rb +289 -0
- data/opal/fron-ui/examples/comments/components/comment.rb +75 -0
- data/opal/fron-ui/examples/comments/components/comments.rb +93 -0
- data/opal/fron-ui/examples/comments/components/footer.rb +36 -0
- data/opal/fron-ui/examples/comments/components/header.rb +35 -0
- data/opal/fron-ui/examples/comments/components/list.rb +12 -0
- data/opal/fron-ui/examples/comments/index.rb +6 -0
- data/opal/fron-ui/examples/contacts/components/contacts.rb +100 -0
- data/opal/fron-ui/examples/contacts/components/details.rb +92 -0
- data/opal/fron-ui/examples/contacts/components/item.rb +46 -0
- data/opal/fron-ui/examples/contacts/components/list.rb +10 -0
- data/opal/fron-ui/examples/contacts/components/sidebar.rb +30 -0
- data/opal/fron-ui/examples/contacts/index.rb +6 -0
- data/opal/fron-ui/examples/editor/index.rb +164 -0
- data/opal/fron-ui/examples/kitchensink/index.rb +193 -0
- data/opal/fron-ui/examples/todos/components/item.rb +84 -0
- data/opal/fron-ui/examples/todos/components/options.rb +26 -0
- data/opal/fron-ui/examples/todos/components/todos.rb +145 -0
- data/opal/fron-ui/examples/todos/index.rb +6 -0
- data/opal/fron-ui/examples/webshop/index.rb +0 -0
- data/opal/fron-ui/fonts/ionicons.rb +2954 -0
- data/opal/fron-ui/fonts/open_sans.rb +19 -0
- data/opal/fron-ui/lib/collection.rb +138 -0
- data/opal/fron-ui/lib/date.rb +23 -0
- data/opal/fron-ui/lib/debounce.rb +14 -0
- data/opal/fron-ui/lib/image_loader.rb +13 -0
- data/opal/fron-ui/lib/lorem.rb +93 -0
- data/opal/fron-ui/lib/nil.rb +29 -0
- data/opal/fron-ui/lib/record.rb +23 -0
- data/opal/fron-ui/lib/state_serializer.rb +129 -0
- data/opal/fron-ui/lib/storage.rb +57 -0
- data/opal/fron-ui/spec/setup.rb +40 -0
- data/opal/fron-ui/ui.rb +40 -0
- data/opal/fron-ui/utils/theme_roller.rb +63 -0
- data/opal/fron-ui/vendor/autoprefixer.js +21114 -0
- data/opal/fron-ui/vendor/marked.js +1291 -0
- data/opal/fron-ui/vendor/md5.js +274 -0
- data/opal/fron-ui/vendor/moment.js +3083 -0
- data/opal/fron-ui/vendor/uuid.js +92 -0
- data/opal/fron_ui.rb +13 -0
- data/spec/behaviors/action_spec.rb +34 -0
- data/spec/behaviors/actions_spec.rb +38 -0
- data/spec/behaviors/confirmation_spec.rb +23 -0
- data/spec/behaviors/dropdown_spec.rb +32 -0
- data/spec/behaviors/render_spec.rb +20 -0
- data/spec/behaviors/rest_spec.rb +70 -0
- data/spec/behaviors/selectable_children_spec.rb +40 -0
- data/spec/behaviors/serialize_spec.rb +34 -0
- data/spec/components/action_spec.rb +7 -0
- data/spec/components/base_spec.rb +19 -0
- data/spec/components/box_spec.rb +7 -0
- data/spec/components/button_spec.rb +9 -0
- data/spec/components/calendar_spec.rb +58 -0
- data/spec/components/checkbox_spec.rb +20 -0
- data/spec/components/chooser_spec.rb +75 -0
- data/spec/components/color_panel_spec.rb +49 -0
- data/spec/components/color_picker_spec.rb +41 -0
- data/spec/components/container_spec.rb +23 -0
- data/spec/components/date_picker_spec.rb +71 -0
- data/spec/components/drag_spec.rb +20 -0
- data/spec/components/dropdown_spec.rb +33 -0
- data/spec/components/image_spec.rb +33 -0
- data/spec/components/input_spec.rb +8 -0
- data/spec/components/list_spec.rb +10 -0
- data/spec/components/loader_spec.rb +9 -0
- data/spec/components/notifications_spec.rb +17 -0
- data/spec/components/number_spec.rb +64 -0
- data/spec/components/progress_spec.rb +23 -0
- data/spec/components/slider_spec.rb +25 -0
- data/spec/components/tabs_spec.rb +50 -0
- data/spec/components/textarea_spec.rb +7 -0
- data/spec/components/time_spec.rb +37 -0
- data/spec/components/title_spec.rb +11 -0
- data/spec/examples/comments_spec.rb +72 -0
- data/spec/examples/todos_spec.rb +39 -0
- data/spec/lib/collection_spec.rb +38 -0
- data/spec/lib/lorem_spec.rb +55 -0
- data/spec/lib/state_serializer_spec.rb +58 -0
- data/spec/lib/storage_spec.rb +39 -0
- data/spec/spec_helper.rb +1 -0
- metadata +223 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module UI
|
2
|
+
# Simple progressbar
|
3
|
+
#
|
4
|
+
# @author Gusztáv Szikszai
|
5
|
+
# @since 0.1.0
|
6
|
+
class Progress < Base
|
7
|
+
tag 'ui-progress'
|
8
|
+
|
9
|
+
component :bar, 'ui-progress-bar'
|
10
|
+
|
11
|
+
style borderRadius: -> { theme.border_radius.em },
|
12
|
+
minWidth: -> { (theme.size * 6).em },
|
13
|
+
background: -> { colors.input },
|
14
|
+
height: -> { 0.8.em },
|
15
|
+
'ui-progress-bar' => {
|
16
|
+
borderRadius: -> { theme.border_radius.em },
|
17
|
+
background: -> { colors.primary },
|
18
|
+
transition: 'width 320ms',
|
19
|
+
height: :inherit,
|
20
|
+
display: :block
|
21
|
+
}
|
22
|
+
|
23
|
+
# Initializes the component and sets default value
|
24
|
+
def initialize
|
25
|
+
super
|
26
|
+
self.value = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sets the color of the bar
|
30
|
+
#
|
31
|
+
# @param color [String, Symbol] The color
|
32
|
+
def color=(color)
|
33
|
+
@bar.style.backgroundColor = color
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sets the value
|
37
|
+
#
|
38
|
+
# @param value [Float] The value (0..1)
|
39
|
+
def value=(value)
|
40
|
+
value = value.clamp(0, 1) * 100
|
41
|
+
@bar.style.width = "#{value}%"
|
42
|
+
self[:percent] = "#{value}%"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the value
|
46
|
+
#
|
47
|
+
# @return [Float] The value
|
48
|
+
def value
|
49
|
+
@bar.style.width.to_i / 100
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module UI
|
2
|
+
# Horizontal range / slider component
|
3
|
+
#
|
4
|
+
# @attr value [Float] The value
|
5
|
+
#
|
6
|
+
# @author Gusztáv Szikszai
|
7
|
+
# @since 0.1.0
|
8
|
+
class Slider < Drag
|
9
|
+
tag 'ui-slider'
|
10
|
+
|
11
|
+
style minWidth: -> { (theme.size * 6).em },
|
12
|
+
height: -> { theme.size.em },
|
13
|
+
background: :transparent,
|
14
|
+
cursor: :pointer,
|
15
|
+
'ui-drag-handle' => { borderRadius: -> { theme.border_radius.em },
|
16
|
+
background: -> { colors.primary },
|
17
|
+
pointerEvents: :auto,
|
18
|
+
height: 1.5.em,
|
19
|
+
width: 1.5.em,
|
20
|
+
top: '50%' },
|
21
|
+
'&:focus' => { outline: :none,
|
22
|
+
'&:before' => { boxShadow: -> { theme.focus_box_shadow.call } } },
|
23
|
+
'&:before' => { borderRadius: -> { theme.border_radius.em },
|
24
|
+
background: -> { colors.input },
|
25
|
+
position: :absolute,
|
26
|
+
marginTop: -0.4.em,
|
27
|
+
height: 0.8.em,
|
28
|
+
content: "''",
|
29
|
+
top: '50%',
|
30
|
+
right: 0,
|
31
|
+
left: 0 }
|
32
|
+
|
33
|
+
on :mousedown, :focus
|
34
|
+
|
35
|
+
# Initailizes the component by
|
36
|
+
# seting tabindex and restricting
|
37
|
+
# it to be horizontal
|
38
|
+
def initialize
|
39
|
+
super
|
40
|
+
@vertical = false
|
41
|
+
self[:tabindex] ||= 0
|
42
|
+
end
|
43
|
+
|
44
|
+
alias value value_x
|
45
|
+
alias value= value_x=
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module UI
|
2
|
+
# Component for tabular UI.
|
3
|
+
#
|
4
|
+
# @author Gusztáv Szikszai
|
5
|
+
# @since 0.1.0
|
6
|
+
class Tabs < UI::Container
|
7
|
+
# Component for the handle.
|
8
|
+
#
|
9
|
+
# @author Gusztáv Szikszai
|
10
|
+
# @since 0.1.0
|
11
|
+
class Handle < Action
|
12
|
+
include ::Record
|
13
|
+
|
14
|
+
tag 'ui-tab-handle'
|
15
|
+
|
16
|
+
attribute_accessor :tab_id
|
17
|
+
|
18
|
+
style padding: -> { theme.spacing.em },
|
19
|
+
top: -> { theme.border_size.em },
|
20
|
+
display: 'inline-block',
|
21
|
+
position: :relative,
|
22
|
+
fontWeight: 600,
|
23
|
+
opacity: 0.5,
|
24
|
+
'&.selected' => { borderBottom: -> { "#{theme.border_size.em} solid #{colors.primary}" },
|
25
|
+
color: -> { colors.primary } },
|
26
|
+
'&:focus' => { borderBottom: -> { "#{theme.border_size.em} solid #{colors.focus}" },
|
27
|
+
color: -> { colors.focus } },
|
28
|
+
'&:hover, &.selected, &:focus' => { opacity: 1 }
|
29
|
+
|
30
|
+
# Renders the component
|
31
|
+
def render
|
32
|
+
self.tab_id = @data[:id]
|
33
|
+
self.text = @data[:title]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Tab handle container.
|
38
|
+
#
|
39
|
+
# @author Gusztáv Szikszai
|
40
|
+
# @since 0.1.0
|
41
|
+
class Handles < Collection
|
42
|
+
include UI::Behaviors::SelectableChildren
|
43
|
+
|
44
|
+
tag 'ui-tab-handles'
|
45
|
+
|
46
|
+
style display: :block,
|
47
|
+
flex: '0 0 auto',
|
48
|
+
minHeight: 3.em,
|
49
|
+
borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Basic tab container.
|
53
|
+
#
|
54
|
+
# @author Gusztáv Szikszai
|
55
|
+
# @since 0.1.0
|
56
|
+
class Tab < Base
|
57
|
+
tag 'ui-tab'
|
58
|
+
|
59
|
+
style padding: -> { theme.spacing.em }
|
60
|
+
end
|
61
|
+
|
62
|
+
extend Forwardable
|
63
|
+
|
64
|
+
tag 'ui-tabs'
|
65
|
+
|
66
|
+
component :handles, Handles, base: Handle
|
67
|
+
|
68
|
+
def_delegators :handles, :base, :base=
|
69
|
+
|
70
|
+
style '> *:not(ui-tab-handles):not(.active)' => { visibility: :hidden,
|
71
|
+
overflow: :hidden,
|
72
|
+
display: :block,
|
73
|
+
padding: 0,
|
74
|
+
height: 0 },
|
75
|
+
'> .active' => { visibility: :visible,
|
76
|
+
overflow: :auto,
|
77
|
+
display: :block,
|
78
|
+
height: :auto,
|
79
|
+
flex: 1 }
|
80
|
+
|
81
|
+
on :selected_change, :select_tab
|
82
|
+
|
83
|
+
# Monkeypatch to update tabs
|
84
|
+
def <<
|
85
|
+
super
|
86
|
+
update_tabs
|
87
|
+
end
|
88
|
+
|
89
|
+
# Monkeypatch to update tabs
|
90
|
+
def insert_before
|
91
|
+
super
|
92
|
+
update_tabs
|
93
|
+
end
|
94
|
+
|
95
|
+
# Monkeypatch to update tabs
|
96
|
+
def remove
|
97
|
+
super
|
98
|
+
update_tabs
|
99
|
+
end
|
100
|
+
|
101
|
+
# Activates the currently selected tab
|
102
|
+
def select_tab
|
103
|
+
select find_by_id(handles.selected.tab_id)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Selects the given tab.
|
107
|
+
#
|
108
|
+
# @param tab [UI::Tabs::Tab, Fron::Component] The tab
|
109
|
+
def select(tab)
|
110
|
+
return unless tab
|
111
|
+
return if active_tab == tab
|
112
|
+
active_tab.remove_class(:active) if active_tab
|
113
|
+
handles.select handles.find("[tab_id='#{tab[:tab_id]}']")
|
114
|
+
tab.add_class(:active)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the active tab
|
118
|
+
#
|
119
|
+
# @return [UI::Tabs::Tab, Fron::Component] The tab
|
120
|
+
def active_tab
|
121
|
+
children.find { |child| child.has_class(:active) }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the tab by the given id
|
125
|
+
#
|
126
|
+
# @param id [String] The id
|
127
|
+
#
|
128
|
+
# @return [UI::Tabs::Tab, Fron::Component] The tab
|
129
|
+
def find_by_id(id)
|
130
|
+
children.find { |child| child[:tab_id] == id }
|
131
|
+
end
|
132
|
+
|
133
|
+
# Updates tab handles to reflect the content.
|
134
|
+
def update_tabs
|
135
|
+
return unless @handles
|
136
|
+
handles.items = tabs.map { |item| { id: item[:tab_id], title: item[:tab_title] } }.uniq
|
137
|
+
return if handles.selected || handles.children.empty?
|
138
|
+
handles.select_first
|
139
|
+
select_tab
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns all tabs
|
143
|
+
#
|
144
|
+
# @return [Array<UI::Tabs::Tab>] The tabs
|
145
|
+
def tabs
|
146
|
+
find_all('[tab_id][tab_title]')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module UI
|
2
|
+
# Time component to display static times or
|
3
|
+
# times that counts upwards (fromNow), using [Moment.js](http://momentjs.com/).
|
4
|
+
#
|
5
|
+
# @attr from_now [Bolean] The use the fromNow or not
|
6
|
+
# @attr format [String] The format to display
|
7
|
+
#
|
8
|
+
# @author Gusztáv Szikszai
|
9
|
+
# @since 0.1.0
|
10
|
+
class Time < Fron::Component
|
11
|
+
tag 'ui-time'
|
12
|
+
|
13
|
+
attr_accessor :from_now, :format
|
14
|
+
|
15
|
+
# Initializes the component
|
16
|
+
def initialize
|
17
|
+
super
|
18
|
+
render
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sets the value (time) of the component
|
22
|
+
#
|
23
|
+
# @param value [Date, String] The value
|
24
|
+
def value=(value)
|
25
|
+
@value = if value.is_a? Date
|
26
|
+
Native(`moment(#{value}.date)`)
|
27
|
+
else
|
28
|
+
Native(`moment(#{value})`)
|
29
|
+
end
|
30
|
+
rescue
|
31
|
+
@value = nil
|
32
|
+
ensure
|
33
|
+
render
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the value
|
37
|
+
#
|
38
|
+
# @return [Native] The moment wrapped value
|
39
|
+
def value
|
40
|
+
(@value || Native(`moment()`))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Renders the component
|
44
|
+
def render
|
45
|
+
clear_timeout @id
|
46
|
+
request_animation_frame do
|
47
|
+
self.text = if @from_now
|
48
|
+
value.fromNow
|
49
|
+
else
|
50
|
+
value.format(@format || 'YYYY-MM-DD')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
recall
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Periodically calls render
|
59
|
+
def recall
|
60
|
+
@id = timeout 1000 do
|
61
|
+
render
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module UI
|
2
|
+
# Title component
|
3
|
+
#
|
4
|
+
# Features:
|
5
|
+
# * Align text (left / center / right )
|
6
|
+
# * Bottom border
|
7
|
+
#
|
8
|
+
# @author Gusztáv Szikszai
|
9
|
+
# @since 0.1.0
|
10
|
+
class Title < UI::Container
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
tag 'ui-title'
|
14
|
+
|
15
|
+
style borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" },
|
16
|
+
paddingBottom: -> { theme.spacing.em },
|
17
|
+
fontFamily: -> { theme.font_family },
|
18
|
+
height: 2.em,
|
19
|
+
'> span' => { fontSize: '1.6em',
|
20
|
+
fontWeight: 600,
|
21
|
+
flex: 1 }
|
22
|
+
|
23
|
+
component :span, :span
|
24
|
+
|
25
|
+
def_delegators :span, :text, :text=
|
26
|
+
|
27
|
+
# Sets the text align of the component
|
28
|
+
#
|
29
|
+
# @param value [Symbol] The alignment
|
30
|
+
def align=(value)
|
31
|
+
@style.textAlign = value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'fron_ui'
|
2
|
+
|
3
|
+
class Item < UI::Container
|
4
|
+
include UI::Behaviors::Rest
|
5
|
+
include ::Record
|
6
|
+
|
7
|
+
style padding: -> { theme.spacing.em },
|
8
|
+
'> span' => { fontWeight: 600 },
|
9
|
+
small: { opacity: 0.5 },
|
10
|
+
'&.selected' => {
|
11
|
+
color: -> { colors.focus }
|
12
|
+
}
|
13
|
+
|
14
|
+
component :span, :span
|
15
|
+
component :small, :small do
|
16
|
+
component :span, :span, text: 'Created '
|
17
|
+
component :time, UI::Time, from_now: true
|
18
|
+
end
|
19
|
+
|
20
|
+
def render
|
21
|
+
@span.text = @data[:title]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class List < UI::List
|
26
|
+
include UI::Behaviors::SelectableChildren
|
27
|
+
include UI::Behaviors::Rest
|
28
|
+
|
29
|
+
rest url: 'http://localhost:3000/posts'
|
30
|
+
|
31
|
+
def refresh
|
32
|
+
request :get, '' do |items|
|
33
|
+
self.items = items
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Header < UI::Box
|
40
|
+
tag 'ui-header[direction=row]'
|
41
|
+
|
42
|
+
style '> *' => { padding: -> { "0 #{theme.spacing.em}" } }
|
43
|
+
|
44
|
+
component :brand, 'ui-brand', text: 'Blog'
|
45
|
+
component :posts, UI::Action, text: 'Content', action: :content
|
46
|
+
component :posts, UI::Action, text: 'New Post', action: :create
|
47
|
+
end
|
48
|
+
|
49
|
+
class Posts < UI::Container
|
50
|
+
extend Forwardable
|
51
|
+
|
52
|
+
tag 'ui-posts'
|
53
|
+
|
54
|
+
style 'ui-box:last-of-type ui-icon' => { marginRight: -> { theme.spacing.em } },
|
55
|
+
'ui-body' => { overflow: :auto },
|
56
|
+
'ui-title' => {
|
57
|
+
flex: '0 0 2em',
|
58
|
+
'> span' => {
|
59
|
+
overflow: :hidden,
|
60
|
+
textOverflow: :ellipsis,
|
61
|
+
whiteSpace: :nowrap
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
component :box, UI::Box, flex: '0 0 15em' do
|
66
|
+
component :title, UI::Title, text: 'Posts', direction: :row do
|
67
|
+
component :button, UI::Button, type: :success, shape: :square, action: :create do
|
68
|
+
component :icon, UI::Icon, glyph: :plus
|
69
|
+
end
|
70
|
+
end
|
71
|
+
component :list, List, base: Item, flex: 1
|
72
|
+
end
|
73
|
+
|
74
|
+
component :preview, UI::Box, flex: 1 do
|
75
|
+
component :title, UI::Title, direction: :row do
|
76
|
+
component :button, UI::Button, action: :edit do
|
77
|
+
component :icon, UI::Icon, glyph: :edit
|
78
|
+
component :span, :span, text: 'Edit'
|
79
|
+
end
|
80
|
+
component :button, UI::Button, action: :confirm_destroy!, type: :danger do
|
81
|
+
component :icon, UI::Icon, glyph: 'trash-b'
|
82
|
+
component :span, :span, text: 'Delete'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
component :body, 'ui-body'
|
86
|
+
end
|
87
|
+
|
88
|
+
on :selected_change, :select
|
89
|
+
|
90
|
+
def_delegators :box, :list
|
91
|
+
def_delegators :list, :selected, :select_first
|
92
|
+
|
93
|
+
def refresh
|
94
|
+
list.refresh do
|
95
|
+
break if selected
|
96
|
+
select_first
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def select
|
101
|
+
self.selected = selected.data
|
102
|
+
end
|
103
|
+
|
104
|
+
def selected=(data)
|
105
|
+
@preview.title.text = data[:title]
|
106
|
+
@preview.body.html = `marked(#{data[:body].to_s}, { gfm: true, breaks: true })`
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Form < UI::Container
|
111
|
+
include UI::Behaviors::Serialize
|
112
|
+
include UI::Behaviors::Actions
|
113
|
+
include UI::Behaviors::Rest
|
114
|
+
|
115
|
+
extend Forwardable
|
116
|
+
|
117
|
+
tag 'ui-form'
|
118
|
+
|
119
|
+
rest url: 'http://localhost:3000/posts'
|
120
|
+
|
121
|
+
style 'input[type=text]' => { fontSize: 2.em,
|
122
|
+
flex: '0 0 auto',
|
123
|
+
borderRadius: 0,
|
124
|
+
borderBottom: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" } },
|
125
|
+
'ui-container:first-of-type' => {
|
126
|
+
position: :relative,
|
127
|
+
paddingLeft: '50%',
|
128
|
+
marginTop: '0 !important'
|
129
|
+
},
|
130
|
+
'ui-base, textarea' => {
|
131
|
+
boxSizing: 'border-box',
|
132
|
+
fontSize: 16.px,
|
133
|
+
padding: 20.px
|
134
|
+
},
|
135
|
+
'ui-base' => {
|
136
|
+
borderLeft: -> { "#{theme.border_size.em} solid #{dampen colors.background, 0.05}" },
|
137
|
+
overflow: :auto,
|
138
|
+
div: {
|
139
|
+
maxWidth: 800.px,
|
140
|
+
margin: '0 auto',
|
141
|
+
'> *' => {
|
142
|
+
margin: 0,
|
143
|
+
marginBottom: 0.6.em
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
'input, textarea' => {
|
148
|
+
'&:focus' => {
|
149
|
+
boxShadow: :none
|
150
|
+
}
|
151
|
+
},
|
152
|
+
textarea: {
|
153
|
+
position: :absolute,
|
154
|
+
resize: :none,
|
155
|
+
top: 0,
|
156
|
+
left: 0,
|
157
|
+
width: '50%',
|
158
|
+
height: '100%'
|
159
|
+
}
|
160
|
+
|
161
|
+
component :title, UI::Input, name: :title, placeholder: 'Post title...'
|
162
|
+
component :container, UI::Container, flex: 1, direction: :row, compact: true do
|
163
|
+
component :textarea, UI::Textarea, name: :body, placeholder: 'Post body...', spellcheck: false
|
164
|
+
component :preview, UI::Base, flex: 1 do
|
165
|
+
component :div, :div
|
166
|
+
end
|
167
|
+
end
|
168
|
+
component :statusbar, UI::Container, direction: :row, align: :end, flex: '0 0 auto' do
|
169
|
+
component :button, UI::Button, text: 'Save', action: :save
|
170
|
+
end
|
171
|
+
|
172
|
+
def_delegators :container, :preview, :textarea
|
173
|
+
|
174
|
+
on :keyup, :render
|
175
|
+
|
176
|
+
def initialize
|
177
|
+
super
|
178
|
+
textarea.on :scroll do
|
179
|
+
sync
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def sync
|
184
|
+
preview.scroll_top = preview.scroll_height * (textarea.scroll_top / textarea.scroll_height)
|
185
|
+
end
|
186
|
+
|
187
|
+
def load(id, defaults = { title: '', body: '' })
|
188
|
+
if id
|
189
|
+
request :get, id do |data|
|
190
|
+
super data
|
191
|
+
render
|
192
|
+
yield if block_given?
|
193
|
+
end
|
194
|
+
else
|
195
|
+
super defaults
|
196
|
+
render
|
197
|
+
yield if block_given?
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def save
|
202
|
+
if data[:id]
|
203
|
+
update data
|
204
|
+
else
|
205
|
+
create data do |item|
|
206
|
+
trigger :created, id: item[:id]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def render
|
212
|
+
preview.div.html = `marked(#{data[:body].to_s}, { gfm: true, breaks: true })`
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class Main < UI::Container
|
217
|
+
extend Forwardable
|
218
|
+
include UI::Behaviors::Actions
|
219
|
+
include UI::Behaviors::Rest
|
220
|
+
include UI::Behaviors::State
|
221
|
+
include UI::Behaviors::Confirmation
|
222
|
+
|
223
|
+
rest url: 'http://localhost:3000/posts'
|
224
|
+
|
225
|
+
tag 'main'
|
226
|
+
|
227
|
+
style height: '100vh',
|
228
|
+
boxSizing: 'border-box',
|
229
|
+
fontSize: 14.px,
|
230
|
+
padding: -> { theme.spacing.em }
|
231
|
+
|
232
|
+
component :header, Header
|
233
|
+
component :content, UI::Container, direction: :row, flex: 1, compact: true do
|
234
|
+
component :posts, Posts, flex: 1, direction: :row
|
235
|
+
component :form, Form, flex: 1
|
236
|
+
end
|
237
|
+
|
238
|
+
state_changed :state_changed
|
239
|
+
|
240
|
+
confirmation :destroy!, 'Are you sure you want to remove this post?'
|
241
|
+
|
242
|
+
on :created, :created
|
243
|
+
|
244
|
+
def state_changed
|
245
|
+
if state.key?(:id)
|
246
|
+
load(state[:id])
|
247
|
+
else
|
248
|
+
@content.posts.refresh
|
249
|
+
@content.posts.show
|
250
|
+
@content.form.hide
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def state
|
255
|
+
super.to_h
|
256
|
+
end
|
257
|
+
|
258
|
+
def content
|
259
|
+
self.state = {}
|
260
|
+
end
|
261
|
+
|
262
|
+
def destroy!
|
263
|
+
request :delete, @content.posts.selected.data[:id] do
|
264
|
+
@content.posts.refresh
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def edit
|
269
|
+
self.state = state.merge!(id: @content.posts.selected.data[:id])
|
270
|
+
end
|
271
|
+
|
272
|
+
def load(id)
|
273
|
+
@content.form.load id do
|
274
|
+
@content.posts.hide
|
275
|
+
@content.form.show
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def create
|
280
|
+
self.state = { id: nil }
|
281
|
+
end
|
282
|
+
|
283
|
+
def created(event)
|
284
|
+
self.state = { id: event.id }
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
Fron::Sheet.render_style_tag
|
289
|
+
DOM::Document.body << Main.new
|