Class: Ruflet::Rails::NativeApp

Inherits:
Object
  • Object
show all
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

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 = navigation_bar
  @bottom_appbar = 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

#startObject



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