ruflet_rails 0.0.13 → 0.0.14
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 +4 -4
- data/README.md +165 -62
- data/lib/generators/ruflet/install/install_generator.rb +3 -3
- data/lib/ruflet/rails/configuration.rb +13 -7
- data/lib/ruflet/rails/install_support.rb +19 -637
- data/lib/ruflet/rails/native_app/actions.rb +167 -0
- data/lib/ruflet/rails/native_app/customization.rb +118 -0
- data/lib/ruflet/rails/native_app/html_adapter.rb +248 -0
- data/lib/ruflet/rails/native_app/navigation.rb +645 -0
- data/lib/ruflet/rails/native_app/overlays.rb +222 -0
- data/lib/ruflet/rails/native_app/shell.rb +390 -0
- data/lib/ruflet/rails/native_app.rb +25 -222
- data/lib/ruflet/rails/protocol/web_app.rb +1 -1
- data/lib/ruflet/rails/railtie.rb +3 -20
- data/lib/ruflet/rails/view_helpers.rb +205 -0
- data/lib/ruflet/rails.rb +22 -55
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +0 -4
- metadata +14 -15
- data/lib/generators/ruflet/form/form_generator.rb +0 -55
- data/lib/generators/ruflet/scaffold/scaffold_generator.rb +0 -54
- data/lib/ruflet/rails/form_helpers.rb +0 -161
- data/lib/ruflet/rails/generator_hooks.rb +0 -25
- data/lib/ruflet/rails/resource_component.rb +0 -370
- data/lib/ruflet/rails/route_stack.rb +0 -90
- data/lib/ruflet/rails/webview_app.rb +0 -54
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
class NativeApp
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# --- Declared actions --------------------------------------------------
|
|
9
|
+
|
|
10
|
+
# Route a `data-ruflet-action` element to native.
|
|
11
|
+
def dispatch_action(spec)
|
|
12
|
+
component = (spec["component"] || spec["type"]).to_s
|
|
13
|
+
action = spec["action"].to_s
|
|
14
|
+
|
|
15
|
+
case component
|
|
16
|
+
when "navigation", "navigate"
|
|
17
|
+
navigate_screen(spec["url"], action, spec)
|
|
18
|
+
when "request", "form"
|
|
19
|
+
submit_webview_request(spec["url"], spec["method"] || action || "post")
|
|
20
|
+
when "dialog"
|
|
21
|
+
show_native_dialog(spec)
|
|
22
|
+
when "toast"
|
|
23
|
+
show_toast(spec)
|
|
24
|
+
when "sheet"
|
|
25
|
+
present_sheet(spec)
|
|
26
|
+
when "menu"
|
|
27
|
+
show_native_menu(spec)
|
|
28
|
+
when "drawer"
|
|
29
|
+
show_drawer
|
|
30
|
+
when "share"
|
|
31
|
+
share_content(spec)
|
|
32
|
+
when "clipboard", "copy"
|
|
33
|
+
copy_to_clipboard(spec)
|
|
34
|
+
when "launcher", "url", "url_launcher"
|
|
35
|
+
launch_external_url(spec)
|
|
36
|
+
when "haptic", "haptic_feedback"
|
|
37
|
+
haptic_feedback(spec)
|
|
38
|
+
else
|
|
39
|
+
case action
|
|
40
|
+
when "dialog" then show_native_dialog(spec)
|
|
41
|
+
when "toast" then show_toast(spec)
|
|
42
|
+
when "sheet" then present_sheet(spec)
|
|
43
|
+
when "menu" then show_native_menu(spec)
|
|
44
|
+
when "drawer" then show_drawer
|
|
45
|
+
when "end_drawer" then show_end_drawer
|
|
46
|
+
when "share" then share_content(spec)
|
|
47
|
+
when "copy", "clipboard" then copy_to_clipboard(spec)
|
|
48
|
+
when "launch", "url", "url_launcher" then launch_external_url(spec)
|
|
49
|
+
when "haptic", "haptic_feedback" then haptic_feedback(spec)
|
|
50
|
+
when "back" then pop
|
|
51
|
+
when "delete", "post", "patch", "put" then submit_webview_request(spec["url"], action)
|
|
52
|
+
when "navigate" then navigate_screen(spec["url"], spec["mode"] || "push", spec)
|
|
53
|
+
when "push", "replace", "root" then navigate_screen(spec["url"], action, spec)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# --- Native services ---------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def share_content(spec)
|
|
61
|
+
clear_transient_overlays
|
|
62
|
+
files = spec["files"]
|
|
63
|
+
uri = share_uri_for(spec)
|
|
64
|
+
text = spec["text"] || spec["message"] || spec["content"]
|
|
65
|
+
|
|
66
|
+
if files
|
|
67
|
+
@page.share_files(files, text: text, title: spec["title"], subject: spec["subject"])
|
|
68
|
+
elsif uri && text.to_s.empty?
|
|
69
|
+
@page.share_uri(uri)
|
|
70
|
+
else
|
|
71
|
+
@page.share_text(text.to_s, title: spec["title"], subject: spec["subject"])
|
|
72
|
+
end
|
|
73
|
+
haptic_feedback({ "style" => "selection" }) if spec["haptic"]
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def copy_to_clipboard(spec)
|
|
79
|
+
clear_transient_overlays
|
|
80
|
+
value = spec["text"] || spec["value"] || spec["content"] || spec["message"] || spec["url"]
|
|
81
|
+
return if value.to_s.empty?
|
|
82
|
+
|
|
83
|
+
@page.set_clipboard(value)
|
|
84
|
+
show_toast(
|
|
85
|
+
"message" => spec["toast"].to_s,
|
|
86
|
+
"duration" => (spec["toast_duration"] || spec["duration"] || 900).to_s
|
|
87
|
+
) unless spec["toast"].to_s.empty?
|
|
88
|
+
haptic_feedback({ "style" => "selection" }) if spec.fetch("haptic", true)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def share_uri_for(spec)
|
|
94
|
+
uri = spec["uri"] || spec["url"]
|
|
95
|
+
return current_url if uri.to_s.empty? || uri.to_s == "#"
|
|
96
|
+
|
|
97
|
+
uri
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def same_url?(left, right)
|
|
101
|
+
absolute_url(left).to_s == absolute_url(right).to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def clear_transient_overlays
|
|
105
|
+
@page.snackbar = nil if @page.respond_to?(:snackbar=)
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def launch_external_url(spec)
|
|
111
|
+
url = spec["url"] || spec["href"] || spec["uri"]
|
|
112
|
+
return if url.to_s.empty?
|
|
113
|
+
|
|
114
|
+
@page.launch_url(url.to_s, mode: spec["mode"])
|
|
115
|
+
rescue StandardError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def submit_webview_request(url, method = "post")
|
|
120
|
+
target = absolute_url(url)
|
|
121
|
+
return if target.empty?
|
|
122
|
+
|
|
123
|
+
verb = method.to_s.downcase
|
|
124
|
+
verb = "post" if verb.empty?
|
|
125
|
+
js = <<~JS
|
|
126
|
+
(function () {
|
|
127
|
+
var token = document.querySelector("meta[name='csrf-token']");
|
|
128
|
+
var form = document.createElement("form");
|
|
129
|
+
form.method = "post";
|
|
130
|
+
form.action = #{target.to_json};
|
|
131
|
+
form.style.display = "none";
|
|
132
|
+
var methodInput = document.createElement("input");
|
|
133
|
+
methodInput.type = "hidden";
|
|
134
|
+
methodInput.name = "_method";
|
|
135
|
+
methodInput.value = #{verb.to_json};
|
|
136
|
+
form.appendChild(methodInput);
|
|
137
|
+
if (token && token.content) {
|
|
138
|
+
var csrf = document.createElement("input");
|
|
139
|
+
csrf.type = "hidden";
|
|
140
|
+
csrf.name = "authenticity_token";
|
|
141
|
+
csrf.value = token.content;
|
|
142
|
+
form.appendChild(csrf);
|
|
143
|
+
}
|
|
144
|
+
document.body.appendChild(form);
|
|
145
|
+
form.submit();
|
|
146
|
+
})();
|
|
147
|
+
JS
|
|
148
|
+
@screens.last&.webview&.run_javascript(js)
|
|
149
|
+
rescue StandardError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def haptic_feedback(spec)
|
|
154
|
+
style = (spec["style"] || spec["kind"] || spec["impact"] || "selection").to_s
|
|
155
|
+
case style
|
|
156
|
+
when "heavy" then @page.heavy_impact
|
|
157
|
+
when "medium" then @page.medium_impact
|
|
158
|
+
when "light" then @page.light_impact
|
|
159
|
+
when "vibrate" then @page.vibrate
|
|
160
|
+
else @page.selection_click
|
|
161
|
+
end
|
|
162
|
+
rescue StandardError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
class NativeApp
|
|
6
|
+
CONTROL_PROP_KEYS = {
|
|
7
|
+
appbar: %w[
|
|
8
|
+
actions_padding adaptive automatically_imply_leading bgcolor center_title clip_behavior color data elevation
|
|
9
|
+
elevation_on_scroll exclude_header_semantics force_material_transparency leading_width opacity secondary
|
|
10
|
+
shadow_color shape title_spacing title_text_style toolbar_height toolbar_opacity toolbar_text_style tooltip visible
|
|
11
|
+
],
|
|
12
|
+
navigation_bar: %w[
|
|
13
|
+
adaptive bgcolor border elevation height indicator_color indicator_shape label_behavior label_padding margin
|
|
14
|
+
overlay_color shadow_color surface_tint_color tooltip visible width
|
|
15
|
+
],
|
|
16
|
+
navigation_bar_destination: %w[
|
|
17
|
+
adaptive bgcolor data disabled selected_icon tooltip visible
|
|
18
|
+
],
|
|
19
|
+
navigation_drawer: %w[
|
|
20
|
+
adaptive bgcolor elevation indicator_color indicator_shape opacity shadow_color surface_tint_color tile_padding
|
|
21
|
+
tooltip visible width
|
|
22
|
+
],
|
|
23
|
+
list_tile: %w[
|
|
24
|
+
adaptive autofocus bgcolor content_padding dense disabled enable_feedback height horizontal_spacing hover_color
|
|
25
|
+
icon_color is_three_line min_height min_leading_width min_vertical_padding selected_color selected_tile_color
|
|
26
|
+
shape splash_color style subtitle_text_style text_color title_alignment title_text_style tooltip visible
|
|
27
|
+
visual_density
|
|
28
|
+
],
|
|
29
|
+
navigation_rail: %w[
|
|
30
|
+
bgcolor elevation extended group_alignment height indicator_color indicator_shape label_type min_extended_width
|
|
31
|
+
min_width opacity selected_label_text_style tooltip trailing unselected_label_text_style use_indicator visible width
|
|
32
|
+
],
|
|
33
|
+
navigation_rail_destination: %w[
|
|
34
|
+
indicator_color indicator_shape padding selected_icon tooltip visible
|
|
35
|
+
],
|
|
36
|
+
icon_button: %w[
|
|
37
|
+
adaptive alignment autofocus bgcolor disabled disabled_color enable_feedback focus_color height highlight_color
|
|
38
|
+
hover_color icon_color icon_size padding selected selected_icon selected_icon_color size_constraints splash_color
|
|
39
|
+
splash_radius style tooltip visible visual_density width
|
|
40
|
+
],
|
|
41
|
+
icon: %w[color size opacity tooltip visible],
|
|
42
|
+
text: %w[
|
|
43
|
+
color font_family italic max_lines no_wrap opacity selectable semantics_label size style text_align
|
|
44
|
+
theme_style tooltip visible weight
|
|
45
|
+
],
|
|
46
|
+
alert_dialog: %w[
|
|
47
|
+
action_button_padding actions_alignment actions_overflow_button_spacing actions_padding adaptive alignment
|
|
48
|
+
barrier_color bgcolor clip_behavior content_padding content_text_style elevation icon_color icon_padding
|
|
49
|
+
inset_padding modal opacity open scrollable semantics_label shadow_color shape title_padding title_text_style
|
|
50
|
+
tooltip visible
|
|
51
|
+
],
|
|
52
|
+
snack_bar: %w[
|
|
53
|
+
action_overflow_threshold adaptive behavior bgcolor clip_behavior close_icon_color dismiss_direction duration
|
|
54
|
+
elevation margin opacity open padding persist shape show_close_icon tooltip visible width
|
|
55
|
+
],
|
|
56
|
+
bottom_sheet: %w[
|
|
57
|
+
adaptive animation_style barrier_color bgcolor clip_behavior dismissible draggable elevation fullscreen
|
|
58
|
+
maintain_bottom_view_insets_padding opacity open scrollable shape show_drag_handle size_constraints tooltip
|
|
59
|
+
use_safe_area visible
|
|
60
|
+
],
|
|
61
|
+
container: %w[
|
|
62
|
+
alignment bgcolor border border_radius bottom clip_behavior color expand height left margin opacity padding
|
|
63
|
+
right shadow tooltip top visible width
|
|
64
|
+
],
|
|
65
|
+
shimmer: %w[base_color direction duration highlight_color loop period visible]
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
INTERNAL_PROP_KEYS = %w[
|
|
69
|
+
action actions component confirm content files haptic href icon items label leading loading message mode nav
|
|
70
|
+
payload selected subject text title toast toast_duration type uri url value
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def ruflet_props_for(control, source, except: [])
|
|
76
|
+
return {} unless source.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
raw = {}
|
|
79
|
+
raw.merge!(stringify_hash(source["props"])) if source["props"].is_a?(Hash)
|
|
80
|
+
raw.merge!(stringify_hash(source))
|
|
81
|
+
rejected = INTERNAL_PROP_KEYS + Array(except).map(&:to_s)
|
|
82
|
+
allowed = CONTROL_PROP_KEYS.fetch(control).map(&:to_s)
|
|
83
|
+
raw.each_with_object({}) do |(key, value), props|
|
|
84
|
+
next if value.nil?
|
|
85
|
+
next if rejected.include?(key.to_s)
|
|
86
|
+
next unless allowed.include?(key.to_s)
|
|
87
|
+
|
|
88
|
+
props[key.to_sym] = ruflet_stringify_nested(value)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def nested_ruflet_props(source, *keys, control:)
|
|
93
|
+
return {} unless source.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
keys.each do |key|
|
|
96
|
+
value = source[key.to_s]
|
|
97
|
+
return ruflet_props_for(control, value) if value.is_a?(Hash)
|
|
98
|
+
end
|
|
99
|
+
{}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stringify_hash(value)
|
|
103
|
+
value.each_with_object({}) { |(key, nested), out| out[key.to_s] = nested }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def ruflet_stringify_nested(value)
|
|
107
|
+
case value
|
|
108
|
+
when Hash
|
|
109
|
+
value.each_with_object({}) { |(key, nested), out| out[key.to_s] = ruflet_stringify_nested(nested) }
|
|
110
|
+
when Array
|
|
111
|
+
value.map { |nested| ruflet_stringify_nested(nested) }
|
|
112
|
+
else
|
|
113
|
+
value
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
class NativeApp
|
|
6
|
+
# Tiny WebView-side adapter: read ERB-rendered `data-ruflet-*`
|
|
7
|
+
# declarations and report them so Ruby can build normal Ruflet controls.
|
|
8
|
+
HTML_ADAPTER_JS = <<~JS
|
|
9
|
+
(function () {
|
|
10
|
+
var rufletInsideSheet = __RUFLET_INSIDE_SHEET__;
|
|
11
|
+
|
|
12
|
+
function report(kind, value) { console.log("ruflet:" + kind + ":" + value); }
|
|
13
|
+
function attr(el, name) {
|
|
14
|
+
var v = el.getAttribute(name);
|
|
15
|
+
if (v != null) return v;
|
|
16
|
+
v = el.getAttribute("data-" + name);
|
|
17
|
+
if (v != null) return v;
|
|
18
|
+
return el.getAttribute("data-ruflet-" + name);
|
|
19
|
+
}
|
|
20
|
+
function has(el, name) {
|
|
21
|
+
return el.hasAttribute(name) || el.hasAttribute("data-" + name) || el.hasAttribute("data-ruflet-" + name);
|
|
22
|
+
}
|
|
23
|
+
function readJSON(raw) {
|
|
24
|
+
if (!raw) return {};
|
|
25
|
+
try { return JSON.parse(raw); } catch (_) { return {}; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function syncChrome() {
|
|
29
|
+
var bar = document.querySelector("[ruflet-appbar],[data-ruflet-appbar]");
|
|
30
|
+
if (bar) {
|
|
31
|
+
bar.style.display = "none";
|
|
32
|
+
var appbar = readJSON(attr(bar, "ruflet-appbar"));
|
|
33
|
+
var heading = bar.querySelector("h1,h2,.title");
|
|
34
|
+
var title = appbar.title || attr(bar, "ruflet-title") || (heading ? heading.textContent : "") || document.title || "";
|
|
35
|
+
var leading = appbar.leading || null;
|
|
36
|
+
var leadingEl = bar.querySelector("[ruflet-leading],[data-ruflet-leading]");
|
|
37
|
+
if (leadingEl) leading = readJSON(attr(leadingEl, "ruflet-leading"));
|
|
38
|
+
var actions = [];
|
|
39
|
+
bar.querySelectorAll("[ruflet-icon],[data-ruflet-icon]").forEach(function (el) {
|
|
40
|
+
var icon = readJSON(attr(el, "ruflet-icon"));
|
|
41
|
+
// Carry the whole payload through (e.g. the title/leading the
|
|
42
|
+
// action declares for the screen it pushes), then normalize the
|
|
43
|
+
// icon/url/action the AppBar button itself needs.
|
|
44
|
+
var entry = Object.assign({}, icon);
|
|
45
|
+
entry.icon = icon.icon || attr(el, "ruflet-icon");
|
|
46
|
+
entry.url = el.getAttribute("href") || icon.url || attr(el, "ruflet-url") || "";
|
|
47
|
+
entry.action = icon.action || attr(el, "ruflet-action") || "push";
|
|
48
|
+
actions.push(entry);
|
|
49
|
+
});
|
|
50
|
+
if (appbar.actions && appbar.actions.length) actions = appbar.actions.concat(actions);
|
|
51
|
+
report("appbar", JSON.stringify({ title: (title || "").trim(), leading: leading, actions: actions }));
|
|
52
|
+
} else {
|
|
53
|
+
report("appbar", JSON.stringify({ absent: true }));
|
|
54
|
+
}
|
|
55
|
+
var nav = document.querySelector("[ruflet-tabs],[data-ruflet-tabs]");
|
|
56
|
+
if (nav) {
|
|
57
|
+
nav.style.display = "none";
|
|
58
|
+
var navSpec = readJSON(attr(nav, "ruflet-tabs"));
|
|
59
|
+
var items = [];
|
|
60
|
+
nav.querySelectorAll("a[href]").forEach(function (a) {
|
|
61
|
+
var icon = readJSON(attr(a, "ruflet-icon"));
|
|
62
|
+
var entry = Object.assign({}, icon);
|
|
63
|
+
entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim();
|
|
64
|
+
entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle";
|
|
65
|
+
entry.url = a.getAttribute("href");
|
|
66
|
+
entry.selected = has(a, "ruflet-selected");
|
|
67
|
+
items.push(entry);
|
|
68
|
+
});
|
|
69
|
+
if (items.length >= 2) report("bottomnav", JSON.stringify(Object.assign({}, navSpec, { items: items })));
|
|
70
|
+
} else {
|
|
71
|
+
report("bottomnav", JSON.stringify({ absent: true }));
|
|
72
|
+
}
|
|
73
|
+
var drawer = document.querySelector("[ruflet-drawer],[data-ruflet-drawer]");
|
|
74
|
+
if (drawer) {
|
|
75
|
+
drawer.style.display = "none";
|
|
76
|
+
var drawerSpec = readJSON(attr(drawer, "ruflet-drawer"));
|
|
77
|
+
var drawerItems = [];
|
|
78
|
+
drawer.querySelectorAll("a[href]").forEach(function (a) {
|
|
79
|
+
var icon = readJSON(attr(a, "ruflet-icon"));
|
|
80
|
+
var entry = Object.assign({}, icon);
|
|
81
|
+
entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim();
|
|
82
|
+
entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle";
|
|
83
|
+
entry.url = a.getAttribute("href");
|
|
84
|
+
entry.action = icon.action || attr(a, "ruflet-action") || drawerSpec.action || "root";
|
|
85
|
+
entry.selected = has(a, "ruflet-selected");
|
|
86
|
+
drawerItems.push(entry);
|
|
87
|
+
});
|
|
88
|
+
if (drawerItems.length) report("drawer", JSON.stringify(Object.assign({}, drawerSpec, { items: drawerItems })));
|
|
89
|
+
} else {
|
|
90
|
+
report("drawer", JSON.stringify({ absent: true }));
|
|
91
|
+
}
|
|
92
|
+
var rail = document.querySelector("[ruflet-rail],[data-ruflet-rail]");
|
|
93
|
+
if (rail) {
|
|
94
|
+
rail.style.display = "none";
|
|
95
|
+
var railSpec = readJSON(attr(rail, "ruflet-rail"));
|
|
96
|
+
if (railSpec.breakpoint && window.innerWidth < Number(railSpec.breakpoint)) {
|
|
97
|
+
report("rail", JSON.stringify({ absent: true }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
var railItems = [];
|
|
101
|
+
rail.querySelectorAll("a[href]").forEach(function (a) {
|
|
102
|
+
var icon = readJSON(attr(a, "ruflet-icon"));
|
|
103
|
+
var entry = Object.assign({}, icon);
|
|
104
|
+
entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim();
|
|
105
|
+
entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle";
|
|
106
|
+
entry.url = a.getAttribute("href");
|
|
107
|
+
entry.action = icon.action || attr(a, "ruflet-action") || railSpec.action || "root";
|
|
108
|
+
entry.selected = has(a, "ruflet-selected");
|
|
109
|
+
railItems.push(entry);
|
|
110
|
+
});
|
|
111
|
+
if (railItems.length >= 2) report("rail", JSON.stringify(Object.assign({}, railSpec, { items: railItems })));
|
|
112
|
+
} else {
|
|
113
|
+
report("rail", JSON.stringify({ absent: true }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
syncChrome();
|
|
117
|
+
|
|
118
|
+
if (window.__rufletHtmlAdapterBound) return;
|
|
119
|
+
window.__rufletHtmlAdapterBound = true;
|
|
120
|
+
|
|
121
|
+
document.addEventListener("click", function (e) {
|
|
122
|
+
var t = e.target;
|
|
123
|
+
var nav = t && t.closest ? t.closest("[ruflet-screen],[data-ruflet-screen]") : null;
|
|
124
|
+
if (nav) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
var spec = readJSON(attr(nav, "ruflet-screen"));
|
|
127
|
+
spec.component = spec.component || "navigation";
|
|
128
|
+
spec.url = nav.getAttribute("href") || spec.url || attr(nav, "ruflet-url") || "";
|
|
129
|
+
report("action", JSON.stringify(spec));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var el = t && t.closest ? t.closest("[ruflet-action],[data-ruflet-action]") : null;
|
|
134
|
+
if (el) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
var payload = readJSON(attr(el, "ruflet-action"));
|
|
137
|
+
payload.url = el.getAttribute("href") || payload.url || attr(el, "ruflet-url") || "";
|
|
138
|
+
report("action", JSON.stringify(payload));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (rufletInsideSheet) {
|
|
143
|
+
var link = t && t.closest ? t.closest("a[href]") : null;
|
|
144
|
+
if (link) {
|
|
145
|
+
var href = link.getAttribute("href") || "";
|
|
146
|
+
var target = link.getAttribute("target") || "";
|
|
147
|
+
if (href && href !== "#" && target !== "_blank" && !/^(mailto:|tel:|sms:|javascript:)/i.test(href)) {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
var linkSpec = readJSON(attr(link, "ruflet-link"));
|
|
150
|
+
linkSpec.component = linkSpec.component || "navigation";
|
|
151
|
+
linkSpec.action = linkSpec.action || attr(link, "ruflet-mode") || attr(link, "ruflet-nav") || "root";
|
|
152
|
+
linkSpec.url = href;
|
|
153
|
+
linkSpec.label = linkSpec.label || link.textContent.trim();
|
|
154
|
+
if (attr(link, "ruflet-close") != null) linkSpec.close = attr(link, "ruflet-close");
|
|
155
|
+
report("action", JSON.stringify(linkSpec));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}, true);
|
|
161
|
+
})();
|
|
162
|
+
JS
|
|
163
|
+
|
|
164
|
+
ACTION_PREFIX = "ruflet:action:"
|
|
165
|
+
APPBAR_PREFIX = "ruflet:appbar:"
|
|
166
|
+
BOTTOMNAV_PREFIX = "ruflet:bottomnav:"
|
|
167
|
+
DRAWER_PREFIX = "ruflet:drawer:"
|
|
168
|
+
RAIL_PREFIX = "ruflet:rail:"
|
|
169
|
+
|
|
170
|
+
def self.html_adapter_js(sheet: false)
|
|
171
|
+
HTML_ADAPTER_JS.sub("__RUFLET_INSIDE_SHEET__", sheet ? "true" : "false")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
# --- HTML adapter ------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def inject_html_adapter(screen)
|
|
179
|
+
inject_html_adapter_for(screen.webview)
|
|
180
|
+
rescue StandardError
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def inject_html_adapter_for(webview, sheet: false)
|
|
185
|
+
webview.run_javascript(self.class.html_adapter_js(sheet: sheet)) if webview.respond_to?(:run_javascript)
|
|
186
|
+
rescue StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Force JavaScript on for a (mounted) webview. The mobile client only
|
|
191
|
+
# enables JS through this invoke method — the enable_javascript prop is a
|
|
192
|
+
# no-op there — so we call it on every page start.
|
|
193
|
+
def enable_js(webview)
|
|
194
|
+
webview.set_javascript_mode("unrestricted") if webview.respond_to?(:set_javascript_mode)
|
|
195
|
+
rescue StandardError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def handle_message(screen, message)
|
|
200
|
+
return if message.nil?
|
|
201
|
+
|
|
202
|
+
if message.start_with?(ACTION_PREFIX)
|
|
203
|
+
spec = parse_json(message[ACTION_PREFIX.length..])
|
|
204
|
+
dispatch_action(spec) if spec
|
|
205
|
+
elsif message.start_with?(APPBAR_PREFIX)
|
|
206
|
+
spec = parse_json(message[APPBAR_PREFIX.length..])
|
|
207
|
+
apply_appbar(screen, spec) if spec
|
|
208
|
+
elsif message.start_with?(BOTTOMNAV_PREFIX)
|
|
209
|
+
spec = parse_json(message[BOTTOMNAV_PREFIX.length..])
|
|
210
|
+
apply_bottomnav(spec) if spec
|
|
211
|
+
elsif message.start_with?(DRAWER_PREFIX)
|
|
212
|
+
spec = parse_json(message[DRAWER_PREFIX.length..])
|
|
213
|
+
apply_drawer(screen, spec) if spec
|
|
214
|
+
elsif message.start_with?(RAIL_PREFIX)
|
|
215
|
+
spec = parse_json(message[RAIL_PREFIX.length..])
|
|
216
|
+
apply_rail(screen, spec) if spec
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def handle_sheet_message(message)
|
|
221
|
+
return if message.nil?
|
|
222
|
+
|
|
223
|
+
if message.start_with?(ACTION_PREFIX)
|
|
224
|
+
spec = parse_json(message[ACTION_PREFIX.length..])
|
|
225
|
+
if spec
|
|
226
|
+
if close_sheet_for?(spec)
|
|
227
|
+
close_sheet { dispatch_action(spec) }
|
|
228
|
+
else
|
|
229
|
+
dispatch_action(spec)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
elsif message.start_with?(APPBAR_PREFIX) || message.start_with?(BOTTOMNAV_PREFIX) ||
|
|
233
|
+
message.start_with?(DRAWER_PREFIX) || message.start_with?(RAIL_PREFIX)
|
|
234
|
+
# Chrome declarations inside a modal sheet describe the sheet body,
|
|
235
|
+
# not the app shell. Ignore them; the host screen owns native chrome.
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def parse_json(raw)
|
|
241
|
+
value = JSON.parse(raw.to_s)
|
|
242
|
+
value.is_a?(Hash) ? value : nil
|
|
243
|
+
rescue JSON::ParserError
|
|
244
|
+
nil
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|