app/models/coupdoeil/popover.rb



# frozen_string_literal: true

module Coupdoeil
  class Popover < AbstractController::Base
    abstract!

    include AbstractController::Rendering

    include AbstractController::Logger
    include AbstractController::Helpers
    include AbstractController::Translation
    include AbstractController::Callbacks
    include AbstractController::Caching

    include ActionView::Layouts
    include ActionView::Rendering

    include Rails.application.routes.url_helpers

    layout "popover"

    # so coupdoeil helpers are always available within popovers
    helper Coupdoeil::ApplicationHelper

    @registry = Registry.new
    @default_options_by_method = {}.with_indifferent_access

    DEFAULT_OPTIONS_KEY = Object.new
    private_constant :DEFAULT_OPTIONS_KEY

    @default_options_by_method[DEFAULT_OPTIONS_KEY] = OptionsSet.new(
      offset: 0,
      placement: "auto",
      animation: "slide-in",
      cache: true,
      loading: :async,
      trigger: "hover",
      opening_delay: true,
    )

    DoubleRenderError = Class.new(::AbstractController::DoubleRenderError)

    # See engine initialization for view paths

    class << self
      attr_reader :registry

      def popover_resource_name = @popover_resource_name ||= name.delete_suffix("Popover").underscore
      def with(...) = setup_class.new(self).with_params(...)

      def inherited(subclass)
        super
        Coupdoeil::Popover.registry.register(subclass.popover_resource_name, subclass)
        subclass.instance_variable_set(:@default_options_by_method, @default_options_by_method.dup)
      end

      def default_options(...) = default_options_for(DEFAULT_OPTIONS_KEY, ...)

      def default_options_for(*action_names, **option_values)
        if option_values.blank?
          @default_options_by_method[action_names.first] || default_options
        else
          action_names.each do |action_name|
            options = @default_options_by_method[action_name] || default_options
            @default_options_by_method[action_name] = options.merge(OptionsSet.new(option_values))
          end
        end
      end

      def method_missing(method_name, *args, &)
        return super unless action_methods.include?(method_name.name)

        action_methods.each do |action_name|
          define_singleton_method(action_name) { setup_class.new(self).with_type(action_name) }
        end
        public_send(method_name)
      end

      def respond_to_missing?(method, include_all = false)
        action_methods.include?(method.name) || super
      end

      def setup_class
        @setup_class ||= begin
          setup_klass = Class.new(Setup)
          action_methods.each do |action_name|
            setup_klass.define_method(action_name) { with_type(action_name) }
          end
          setup_klass
        end
      end
    end

    attr_reader :params

    def initialize(params, cp_view_context)
      super()
      @params = params
      @__cp_view_context = cp_view_context
    end

    def helpers = @__cp_view_context
    def controller = @__cp_view_context.controller

    def view_context
      super.tap do |context|
        context.extend(ViewContextDelegation)
        context.popover = self
        context.__cp_view_context = @__cp_view_context
      end
    end

    def render(...)
      return super unless response_body

      raise DoubleRenderError, "Render was called multiple times in this action. \
Also note that render does not terminate execution of the action."
    end

    def process(method_name, *)
      benchmark("processed popover #{self.class.popover_resource_name}/#{method_name}", silence: true) do
        ActiveSupport::Notifications.instrument("render_popover.coupdoeil") do
          super
          response_body || render(action_name)
        end
      end
    end
  end
end