Class: Ruflet::Rails::NativeApp
- Inherits:
-
Object
- Object
- Ruflet::Rails::NativeApp
- Defined in:
- lib/ruflet/rails/native_app.rb,
lib/ruflet/rails/native_app/shell.rb,
lib/ruflet/rails/native_app/actions.rb,
lib/ruflet/rails/native_app/overlays.rb,
lib/ruflet/rails/native_app/navigation.rb,
lib/ruflet/rails/native_app/html_adapter.rb,
lib/ruflet/rails/native_app/customization.rb
Overview
Managed native shell for a Rails WebView body.
Defined Under Namespace
Classes: Screen
Constant Summary collapse
- HTML_ADAPTER_JS =
Tiny WebView-side adapter: read ERB-rendered
data-ruflet-*declarations and report them so Ruby can build normal Ruflet controls. <<~JS (function () { var rufletInsideSheet = __RUFLET_INSIDE_SHEET__; function report(kind, value) { console.log("ruflet:" + kind + ":" + value); } function attr(el, name) { var v = el.getAttribute(name); if (v != null) return v; v = el.getAttribute("data-" + name); if (v != null) return v; return el.getAttribute("data-ruflet-" + name); } function has(el, name) { return el.hasAttribute(name) || el.hasAttribute("data-" + name) || el.hasAttribute("data-ruflet-" + name); } function readJSON(raw) { if (!raw) return {}; try { return JSON.parse(raw); } catch (_) { return {}; } } function syncChrome() { var bar = document.querySelector("[ruflet-appbar],[data-ruflet-appbar]"); if (bar) { bar.style.display = "none"; var appbar = readJSON(attr(bar, "ruflet-appbar")); var heading = bar.querySelector("h1,h2,.title"); var title = appbar.title || attr(bar, "ruflet-title") || (heading ? heading.textContent : "") || document.title || ""; var leading = appbar.leading || null; var leadingEl = bar.querySelector("[ruflet-leading],[data-ruflet-leading]"); if (leadingEl) leading = readJSON(attr(leadingEl, "ruflet-leading")); var actions = []; bar.querySelectorAll("[ruflet-icon],[data-ruflet-icon]").forEach(function (el) { var icon = readJSON(attr(el, "ruflet-icon")); // Carry the whole payload through (e.g. the title/leading the // action declares for the screen it pushes), then normalize the // icon/url/action the AppBar button itself needs. var entry = Object.assign({}, icon); entry.icon = icon.icon || attr(el, "ruflet-icon"); entry.url = el.getAttribute("href") || icon.url || attr(el, "ruflet-url") || ""; entry.action = icon.action || attr(el, "ruflet-action") || "push"; actions.push(entry); }); if (appbar.actions && appbar.actions.length) actions = appbar.actions.concat(actions); report("appbar", JSON.stringify({ title: (title || "").trim(), leading: leading, actions: actions })); } else { report("appbar", JSON.stringify({ absent: true })); } var nav = document.querySelector("[ruflet-tabs],[data-ruflet-tabs]"); if (nav) { nav.style.display = "none"; var navSpec = readJSON(attr(nav, "ruflet-tabs")); var items = []; nav.querySelectorAll("a[href]").forEach(function (a) { var icon = readJSON(attr(a, "ruflet-icon")); var entry = Object.assign({}, icon); entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim(); entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle"; entry.url = a.getAttribute("href"); entry.selected = has(a, "ruflet-selected"); items.push(entry); }); if (items.length >= 2) report("bottomnav", JSON.stringify(Object.assign({}, navSpec, { items: items }))); } else { report("bottomnav", JSON.stringify({ absent: true })); } var drawer = document.querySelector("[ruflet-drawer],[data-ruflet-drawer]"); if (drawer) { drawer.style.display = "none"; var drawerSpec = readJSON(attr(drawer, "ruflet-drawer")); var drawerItems = []; drawer.querySelectorAll("a[href]").forEach(function (a) { var icon = readJSON(attr(a, "ruflet-icon")); var entry = Object.assign({}, icon); entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim(); entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle"; entry.url = a.getAttribute("href"); entry.action = icon.action || attr(a, "ruflet-action") || drawerSpec.action || "root"; entry.selected = has(a, "ruflet-selected"); drawerItems.push(entry); }); if (drawerItems.length) report("drawer", JSON.stringify(Object.assign({}, drawerSpec, { items: drawerItems }))); } else { report("drawer", JSON.stringify({ absent: true })); } var rail = document.querySelector("[ruflet-rail],[data-ruflet-rail]"); if (rail) { rail.style.display = "none"; var railSpec = readJSON(attr(rail, "ruflet-rail")); if (railSpec.breakpoint && window.innerWidth < Number(railSpec.breakpoint)) { report("rail", JSON.stringify({ absent: true })); return; } var railItems = []; rail.querySelectorAll("a[href]").forEach(function (a) { var icon = readJSON(attr(a, "ruflet-icon")); var entry = Object.assign({}, icon); entry.label = icon.label || attr(a, "ruflet-label") || a.textContent.trim(); entry.icon = icon.icon || attr(a, "ruflet-icon") || "circle"; entry.url = a.getAttribute("href"); entry.action = icon.action || attr(a, "ruflet-action") || railSpec.action || "root"; entry.selected = has(a, "ruflet-selected"); railItems.push(entry); }); if (railItems.length >= 2) report("rail", JSON.stringify(Object.assign({}, railSpec, { items: railItems }))); } else { report("rail", JSON.stringify({ absent: true })); } } syncChrome(); if (window.__rufletHtmlAdapterBound) return; window.__rufletHtmlAdapterBound = true; document.addEventListener("click", function (e) { var t = e.target; var nav = t && t.closest ? t.closest("[ruflet-screen],[data-ruflet-screen]") : null; if (nav) { e.preventDefault(); var spec = readJSON(attr(nav, "ruflet-screen")); spec.component = spec.component || "navigation"; spec.url = nav.getAttribute("href") || spec.url || attr(nav, "ruflet-url") || ""; report("action", JSON.stringify(spec)); return; } var el = t && t.closest ? t.closest("[ruflet-action],[data-ruflet-action]") : null; if (el) { e.preventDefault(); var payload = readJSON(attr(el, "ruflet-action")); payload.url = el.getAttribute("href") || payload.url || attr(el, "ruflet-url") || ""; report("action", JSON.stringify(payload)); return; } if (rufletInsideSheet) { var link = t && t.closest ? t.closest("a[href]") : null; if (link) { var href = link.getAttribute("href") || ""; var target = link.getAttribute("target") || ""; if (href && href !== "#" && target !== "_blank" && !/^(mailto:|tel:|sms:|javascript:)/i.test(href)) { e.preventDefault(); var linkSpec = readJSON(attr(link, "ruflet-link")); linkSpec.component = linkSpec.component || "navigation"; linkSpec.action = linkSpec.action || attr(link, "ruflet-mode") || attr(link, "ruflet-nav") || "root"; linkSpec.url = href; linkSpec.label = linkSpec.label || link.textContent.trim(); if (attr(link, "ruflet-close") != null) linkSpec.close = attr(link, "ruflet-close"); report("action", JSON.stringify(linkSpec)); return; } } } }, true); })(); JS
- ACTION_PREFIX =
"ruflet:action:"- APPBAR_PREFIX =
"ruflet:appbar:"- BOTTOMNAV_PREFIX =
"ruflet:bottomnav:"- DRAWER_PREFIX =
"ruflet:drawer:"- RAIL_PREFIX =
"ruflet:rail:"- CONTROL_PROP_KEYS =
{ appbar: %w[ actions_padding adaptive automatically_imply_leading bgcolor center_title clip_behavior color data elevation elevation_on_scroll exclude_header_semantics force_material_transparency leading_width opacity secondary shadow_color shape title_spacing title_text_style toolbar_height toolbar_opacity toolbar_text_style tooltip visible ], navigation_bar: %w[ adaptive bgcolor border elevation height indicator_color indicator_shape label_behavior label_padding margin overlay_color shadow_color surface_tint_color tooltip visible width ], navigation_bar_destination: %w[ adaptive bgcolor data disabled selected_icon tooltip visible ], navigation_drawer: %w[ adaptive bgcolor elevation indicator_color indicator_shape opacity shadow_color surface_tint_color tile_padding tooltip visible width ], list_tile: %w[ adaptive autofocus bgcolor content_padding dense disabled enable_feedback height horizontal_spacing hover_color icon_color is_three_line min_height min_leading_width min_vertical_padding selected_color selected_tile_color shape splash_color style subtitle_text_style text_color title_alignment title_text_style tooltip visible visual_density ], navigation_rail: %w[ bgcolor elevation extended group_alignment height indicator_color indicator_shape label_type min_extended_width min_width opacity selected_label_text_style tooltip trailing unselected_label_text_style use_indicator visible width ], navigation_rail_destination: %w[ indicator_color indicator_shape padding selected_icon tooltip visible ], icon_button: %w[ adaptive alignment autofocus bgcolor disabled disabled_color enable_feedback focus_color height highlight_color hover_color icon_color icon_size padding selected selected_icon selected_icon_color size_constraints splash_color splash_radius style tooltip visible visual_density width ], icon: %w[color size opacity tooltip visible], text: %w[ color font_family italic max_lines no_wrap opacity selectable semantics_label size style text_align theme_style tooltip visible weight ], alert_dialog: %w[ action_button_padding actions_alignment actions_overflow_button_spacing actions_padding adaptive alignment barrier_color bgcolor clip_behavior content_padding content_text_style elevation icon_color icon_padding inset_padding modal opacity open scrollable semantics_label shadow_color shape title_padding title_text_style tooltip visible ], snack_bar: %w[ action_overflow_threshold adaptive behavior bgcolor clip_behavior close_icon_color dismiss_direction duration elevation margin opacity open padding persist shape show_close_icon tooltip visible width ], bottom_sheet: %w[ adaptive animation_style barrier_color bgcolor clip_behavior dismissible draggable elevation fullscreen maintain_bottom_view_insets_padding opacity open scrollable shape show_drag_handle size_constraints tooltip use_safe_area visible ], container: %w[ alignment bgcolor border border_radius bottom clip_behavior color expand height left margin opacity padding right shadow tooltip top visible width ], shimmer: %w[base_color direction duration highlight_color loop period visible] }.freeze
- INTERNAL_PROP_KEYS =
%w[ action actions component confirm content files haptic href icon items label leading loading message mode nav payload selected subject text title toast toast_duration type uri url value ].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(page, start_url:, title: nil, actions: nil, navigation_bar: nil, bottom_appbar: nil, loading: :shimmer) ⇒ NativeApp
constructor
A new instance of NativeApp.
- #start ⇒ Object
Constructor Details
#initialize(page, start_url:, title: nil, actions: nil, navigation_bar: nil, bottom_appbar: nil, loading: :shimmer) ⇒ NativeApp
Returns a new instance of NativeApp.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/ruflet/rails/native_app.rb', line 13 def initialize(page, start_url:, title: nil, actions: nil, navigation_bar: nil, bottom_appbar: nil, loading: :shimmer) @page = page @start_url = start_url.to_s @title = title.to_s @actions = actions @navigation_bar = @bottom_appbar = @loading = loading @screens = [] @sheet = nil @dialog = nil @bottomnav_signature = nil @bottomnav_spec = nil @appbar_specs_by_url = {} @drawer_spec = nil @drawer_open_requested = false end |
Class Method Details
.html_adapter_js(sheet: false) ⇒ Object
170 171 172 |
# File 'lib/ruflet/rails/native_app/html_adapter.rb', line 170 def self.html_adapter_js(sheet: false) HTML_ADAPTER_JS.sub("__RUFLET_INSIDE_SHEET__", sheet ? "true" : "false") end |
Instance Method Details
#start ⇒ Object
32 33 34 35 36 |
# File 'lib/ruflet/rails/native_app.rb', line 32 def start @page.on_view_pop = ->(_event) { pop } push_webview(@start_url, root: true) self end |