coupdoeil 1.0.0.pre.alpha.1

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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +6 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/config/coupdoeil_manifest.js +1 -0
  7. data/app/assets/javascripts/coupdoeil.js +2289 -0
  8. data/app/assets/javascripts/coupdoeil.min.js +2 -0
  9. data/app/assets/javascripts/coupdoeil.min.js.map +1 -0
  10. data/app/assets/stylesheets/coupdoeil/application.css +15 -0
  11. data/app/assets/stylesheets/coupdoeil/hovercard-animation.css +44 -0
  12. data/app/assets/stylesheets/coupdoeil/hovercard-arrow.css +39 -0
  13. data/app/assets/stylesheets/coupdoeil/hovercard.css +84 -0
  14. data/app/controllers/coupdoeil/hovercards_controller.rb +46 -0
  15. data/app/helpers/coupdoeil/application_helper.rb +16 -0
  16. data/app/javascript/coupdoeil/elements/coupdoeil_element.js +33 -0
  17. data/app/javascript/coupdoeil/events/onclick.js +68 -0
  18. data/app/javascript/coupdoeil/events/onmouseover.js +86 -0
  19. data/app/javascript/coupdoeil/events.js +19 -0
  20. data/app/javascript/coupdoeil/hovercard/actions.js +60 -0
  21. data/app/javascript/coupdoeil/hovercard/attributes.js +33 -0
  22. data/app/javascript/coupdoeil/hovercard/cache.js +18 -0
  23. data/app/javascript/coupdoeil/hovercard/closing.js +81 -0
  24. data/app/javascript/coupdoeil/hovercard/config.js +15 -0
  25. data/app/javascript/coupdoeil/hovercard/controller.js +22 -0
  26. data/app/javascript/coupdoeil/hovercard/opening.js +139 -0
  27. data/app/javascript/coupdoeil/hovercard/optionsParser.js +117 -0
  28. data/app/javascript/coupdoeil/hovercard/positioning.js +74 -0
  29. data/app/javascript/coupdoeil/hovercard/state_check.js +11 -0
  30. data/app/javascript/coupdoeil/hovercard.js +6 -0
  31. data/app/javascript/coupdoeil/index.js +1 -0
  32. data/app/models/coupdoeil/hovercard/option/animation.rb +20 -0
  33. data/app/models/coupdoeil/hovercard/option/cache.rb +19 -0
  34. data/app/models/coupdoeil/hovercard/option/loading.rb +19 -0
  35. data/app/models/coupdoeil/hovercard/option/offset.rb +35 -0
  36. data/app/models/coupdoeil/hovercard/option/placement.rb +44 -0
  37. data/app/models/coupdoeil/hovercard/option/trigger.rb +19 -0
  38. data/app/models/coupdoeil/hovercard/option.rb +45 -0
  39. data/app/models/coupdoeil/hovercard/options_set.rb +57 -0
  40. data/app/models/coupdoeil/hovercard/registry.rb +25 -0
  41. data/app/models/coupdoeil/hovercard/setup.rb +44 -0
  42. data/app/models/coupdoeil/hovercard/view_context_delegation.rb +18 -0
  43. data/app/models/coupdoeil/hovercard.rb +115 -0
  44. data/app/models/coupdoeil/params.rb +83 -0
  45. data/app/models/coupdoeil/tag.rb +45 -0
  46. data/app/style/hovercard-animation.scss +44 -0
  47. data/app/style/hovercard-arrow.scss +40 -0
  48. data/app/style/hovercard.scss +2 -0
  49. data/app/views/layouts/coupdoeil/application.html.erb +15 -0
  50. data/config/routes.rb +3 -0
  51. data/lib/coupdoeil/engine.rb +62 -0
  52. data/lib/coupdoeil/version.rb +3 -0
  53. data/lib/coupdoeil.rb +6 -0
  54. data/lib/generators/coupdoeil/hovercard/USAGE +15 -0
  55. data/lib/generators/coupdoeil/hovercard/hovercard_generator.rb +22 -0
  56. data/lib/generators/coupdoeil/hovercard/templates/hovercard.rb.tt +8 -0
  57. data/lib/generators/coupdoeil/install/install_generator.rb +71 -0
  58. data/lib/generators/coupdoeil/install/templates/layout.html.erb.tt +14 -0
  59. data/lib/tasks/coupdoeil_tasks.rake +4 -0
  60. metadata +129 -0
@@ -0,0 +1,117 @@
1
+ const OPTIONS = {
2
+ animation: { getter: getAnimation },
3
+ cache: { getter: getCache },
4
+ offset: { getter: getOffset },
5
+ placement: { getter: getPlacement },
6
+ loading: { getter: getLoading },
7
+ trigger: { getter: getTrigger }
8
+ }
9
+
10
+ const ORDERED_OPTIONS = [
11
+ "trigger", // bit size: 1 shift: 0
12
+ "loading", // bit size: 2 shift: 1
13
+ "cache", // bit size: 1 shift: 3
14
+ "animation", // bit size: 3 shift: 4
15
+ "placement", // bit size: 16 shift: 7
16
+ "offset" // bit size: 21 shift: 23
17
+ ]
18
+
19
+ const TRIGGERS = ["hover", "click"]
20
+ const ANIMATIONS = [false, "slide-in", "fade-in", "slide-out", "custom"]
21
+ const PLACEMENTS = [
22
+ 'auto',
23
+ 'top', 'top-start', 'top-end',
24
+ 'right', 'right-start', 'right-end',
25
+ 'bottom', 'bottom-start', 'bottom-end',
26
+ 'left', 'left-start', 'left-end'
27
+ ]
28
+ const LOADINGS = ["asyn", "preload", "lazy"]
29
+
30
+ function parseCSSSize(value) {
31
+ if (typeof value === 'number') {
32
+ return value
33
+ } else if ((/^(-?\d+\.?\d+)px$/).test(value)) {
34
+ return parseFloat(value)
35
+ } else if ((/^(-?\d*\.?\d+)rem$/).test(value)) {
36
+ return parseFloat(value) * parseFloat(getComputedStyle(document.documentElement).fontSize)
37
+ }
38
+ return 0
39
+ }
40
+
41
+ function getOffset(optionsInt) {
42
+ // shift is BigInt(16 + 3 + 1 + 2 + 1)
43
+ const offsetBits = Number(BigInt(optionsInt) >> BigInt(23))
44
+ if (offsetBits === 0)
45
+ return 0
46
+
47
+ const isNegative = (offsetBits & 1) === 1
48
+ const isREM = (offsetBits & 2) === 2
49
+ // decimals mask is 2 ** 11 - 1
50
+ const decimals = (offsetBits >> 2) & 2047
51
+ const integer = (offsetBits >> (2 + 11))
52
+
53
+ const CSSSize = `${isNegative ? '-' : ''}${integer}.${decimals}${isREM ? 'rem' : 'px'}`
54
+ return parseCSSSize(CSSSize)
55
+ }
56
+
57
+ function getPlacement(optionsInt) {
58
+ // shift is 3 + 1 + 2 + 1, mask is 2 ** 16 - 1
59
+ const placementBits = (optionsInt >> 7) & 65535
60
+ let shift = 0
61
+ let lastPlacement = null
62
+ const placements = []
63
+ while (lastPlacement !== "auto" && shift < 16) {
64
+ // mask is 2 ** 4 - 1
65
+ lastPlacement = PLACEMENTS[(placementBits >> shift) & 15]
66
+ placements.push(lastPlacement)
67
+ shift += 4
68
+ }
69
+ return placements
70
+ }
71
+
72
+ function getAnimation(optionsInt) {
73
+ return ANIMATIONS[(optionsInt & 56) >> 4]
74
+ }
75
+
76
+ function getCache(optionsInt) {
77
+ return (optionsInt & 8) === 8
78
+ }
79
+
80
+ function getLoading(optionsInt) {
81
+ return LOADINGS[optionsInt & 3 >> 1]
82
+ }
83
+
84
+ function getTrigger(optionsInt) {
85
+ return TRIGGERS[optionsInt & 1]
86
+ }
87
+
88
+ const HovercardOptions = {
89
+ animation: undefined,
90
+ cache: undefined,
91
+ offset: undefined,
92
+ placement: undefined,
93
+ loading: undefined,
94
+ trigger: undefined,
95
+ }
96
+
97
+ export function extractOptionsFromElement(coupdoeilElement) {
98
+ const optionsInt = coupdoeilElement.hovercardController.optionsInt ||= parseOtionsInt((coupdoeilElement))
99
+ const options = Object.create(HovercardOptions)
100
+
101
+ for (const option of ORDERED_OPTIONS) {
102
+ options[option] = OPTIONS[option].getter(optionsInt)
103
+ }
104
+
105
+ return options
106
+ }
107
+
108
+ function parseOtionsInt(coupdoeilElement) {
109
+ const optionsString = coupdoeilElement.getAttribute("hc")
110
+ return parseInt(optionsString, 36)
111
+ }
112
+
113
+ export function extractOptionFromElement(coupdoeilElement, optionName) {
114
+ const optionsInt = coupdoeilElement.hovercardController.optionsInt ||= parseOtionsInt((coupdoeilElement))
115
+
116
+ return OPTIONS[optionName].getter(optionsInt)
117
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ autoPlacement,
3
+ computePosition,
4
+ detectOverflow,
5
+ offset,
6
+ arrow
7
+ } from "@floating-ui/dom"
8
+
9
+ export async function positionHovercard(controller, options) {
10
+ let { placement: placements, offset: offsetValue } = options
11
+ const placement = placements[0]
12
+ const arrowElement = controller.card.querySelector('[data-hovercard-arrow]')
13
+ const middleware = [AutoPositioningWithFallbacks(placements)]
14
+
15
+ if (arrowElement) {
16
+ const arrowSize = arrowElement.clientWidth
17
+ offsetValue += arrowSize
18
+ middleware.push(arrow({ element: arrowElement }))
19
+ }
20
+
21
+ middleware.push(offset(offsetValue))
22
+
23
+ const computedPosition = await computePosition(
24
+ controller.coupdoeilElement, controller.card, { placement, middleware }
25
+ )
26
+ const { x, y, placement: actualPlacement} = computedPosition
27
+
28
+ if (arrowElement) {
29
+ positionArrow(arrowElement, computedPosition)
30
+ }
31
+
32
+ controller.card.dataset.placement = actualPlacement
33
+ Object.assign(controller.card.style, { left: `${x}px`, top: `${y}px` })
34
+ }
35
+
36
+ const AutoPositioningWithFallbacks = (placements) => {
37
+ let placementIndex = 0
38
+ return {
39
+ name: 'autoPlacement',
40
+ async fn(state) {
41
+ if (state.placement === 'auto') {
42
+ return autoPlacement().fn(state)
43
+ }
44
+
45
+ const { top, bottom, left, right } = await detectOverflow(state)
46
+ const isOverflowing = (top > 0 || bottom > 0 || left > 0 || right > 0)
47
+
48
+ if (placements[placementIndex] !== 'auto' && isOverflowing) {
49
+ placementIndex++
50
+ return { reset: { placement: placements[placementIndex] || 'auto' } }
51
+ } else if (isOverflowing) {
52
+ return autoPlacement().fn(state)
53
+ }
54
+ return {}
55
+ }
56
+ }
57
+ }
58
+
59
+ const OPPOSITE_SIDES = { right: 'left', left: 'right', top: 'bottom', bottom: 'top' }
60
+
61
+ function positionArrow(arrowElement, computedData) {
62
+ const arrowSize = arrowElement.clientWidth
63
+ const { placement, middlewareData: { arrow: arrowData } } = computedData
64
+ const side = placement.split("-")[0]
65
+ const oppositeSide = OPPOSITE_SIDES[side]
66
+ const { x, y } = arrowData
67
+ const { borderWidth: parentBorderWidht } = getComputedStyle(arrowElement.parentElement)
68
+
69
+ Object.assign(arrowElement.style, {
70
+ left: x != null ? `${x}px` : '',
71
+ top: y != null ? `${y}px` : '',
72
+ [oppositeSide]: `calc(${-arrowSize}px + ${parentBorderWidht})`
73
+ })
74
+ }
@@ -0,0 +1,11 @@
1
+ import {HOVERCARD_CLOSE_BTN_SELECTOR} from "./config";
2
+ import {currentHovercardsById} from "./actions";
3
+
4
+ export function isElementCloseHovercardButton(element) {
5
+ return element.closest(HOVERCARD_CLOSE_BTN_SELECTOR) ||
6
+ element.dataset.hasOwnProperty("hovercardClose")
7
+ }
8
+
9
+ export function isAnyHovercardOpened() {
10
+ return currentHovercardsById().size > 0
11
+ }
@@ -0,0 +1,6 @@
1
+ import CoupdoeilElement from "./elements/coupdoeil_element"
2
+ import './events'
3
+
4
+ if (customElements.get("coup-doeil") === undefined) {
5
+ customElements.define("coup-doeil", CoupdoeilElement)
6
+ }
@@ -0,0 +1 @@
1
+ import './hovercard'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Animation < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 3
8
+
9
+ VALUES = %w[slide-in fade-in slide-out custom].unshift(false).freeze
10
+ INDEX_BY_VALUES = VALUES.each_with_index.to_h.with_indifferent_access.freeze
11
+
12
+ class << self
13
+ def parse(value) = INDEX_BY_VALUES.fetch(value)
14
+ end
15
+
16
+ def validate! = validate_inclusion!
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Cache < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 1
8
+
9
+ VALUES = [true, false].freeze
10
+
11
+ class << self
12
+ def parse(value) = value ? 1 : 0
13
+ end
14
+
15
+ def validate! = validate_inclusion!
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Loading < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 2
8
+
9
+ VALUES = %w[async preload lazy].freeze
10
+
11
+ class << self
12
+ def parse(value) = VALUES.index(value.to_s)
13
+ end
14
+
15
+ def validate! = validate_inclusion!
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Offset < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 8 + 11 + 2
8
+
9
+ class << self
10
+ def parse(value)
11
+ float_value = value.to_f
12
+ return 0 if float_value.zero?
13
+
14
+ base = 0
15
+ base |= 1 if float_value.negative?
16
+ base |= 2 if value.is_a?(String) && value.end_with?("rem")
17
+
18
+ integer, decimals = float_value.abs.to_s.split(".")
19
+ base |= (decimals.to_i << 2)
20
+ base |= (integer.to_i << 2 + 11)
21
+
22
+ base
23
+ end
24
+ end
25
+
26
+ def validate!
27
+ return if value in Float | Integer
28
+ return if value.to_s.match?(/^-?\d+(\.\d{1,3})?(px|rem)?$/)
29
+
30
+ raise_invalid_option "Value should be a signed float or integer, followed or not by 'rem' or 'px'."
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Placement < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 4 * 4
8
+
9
+ VALUES = %w[
10
+ auto
11
+ top top-start top-end
12
+ right right-start right-end
13
+ bottom bottom-start bottom-end
14
+ left left-start left-end
15
+ ].freeze
16
+ INDEX_BY_VALUES = VALUES.each_with_index.to_h.with_indifferent_access.freeze
17
+
18
+ class << self
19
+ def parse(value)
20
+ values = value.to_s.split(",")
21
+ 4.times.sum do |index|
22
+ next 0 unless (placement = values[index])
23
+
24
+ placement.strip!
25
+ placement_index = INDEX_BY_VALUES[placement]
26
+ placement_index << (index * 4)
27
+ end
28
+ end
29
+ end
30
+
31
+ def validate!
32
+ values = value.to_s.split(",")
33
+
34
+ values.each do |placement_value|
35
+ next if placement_value.strip.in?(VALUES)
36
+
37
+ values_sentence = VALUES.to_sentence(last_word_connector: " or ")
38
+ raise_invalid_option "Value must be one of: #{values_sentence}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class Trigger < Coupdoeil::Hovercard::Option
7
+ self.bit_size = 1
8
+
9
+ VALUES = %w[click hover].freeze
10
+
11
+ class << self
12
+ def parse(value) = value.to_s == "click" ? 1 : 0
13
+ end
14
+
15
+ def validate! = validate_inclusion!
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module Coupdoeil
3
+ class Hovercard
4
+ class Option
5
+ InvalidOptionError = Class.new(StandardError)
6
+
7
+ class << self
8
+ attr_accessor :bit_size, :key
9
+
10
+ def into_bits(value) = parse(value)
11
+
12
+ def inherited(subclass)
13
+ super
14
+ subclass.class_eval do
15
+ @key = name.demodulize.underscore.to_sym
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_reader :value
21
+
22
+ def initialize(value)
23
+ value = value.to_s if value.is_a? Symbol
24
+ @value = value
25
+ end
26
+
27
+ def validate! = raise(NotImplementedError)
28
+
29
+ private
30
+
31
+ def raise_invalid_option(message)
32
+ raise InvalidOptionError,
33
+ "Invalid value '#{value}' (#{value.class.name}) for #{self.class.key} option. #{message}"
34
+ end
35
+
36
+ def validate_inclusion!
37
+ values = self.class.const_get("VALUES")
38
+ return if value.in?(values)
39
+
40
+ values_sentence = values.to_sentence(two_words_connector: " or ", last_word_connector: " or ")
41
+ raise_invalid_option "Value must be one of: #{values_sentence}"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal: true
4
+
5
+ module Coupdoeil
6
+ class Hovercard
7
+ class OptionsSet
8
+ ORDERED_OPTIONS = [
9
+ Option::Offset,
10
+ Option::Placement,
11
+ Option::Animation,
12
+ Option::Cache,
13
+ Option::Loading,
14
+ Option::Trigger
15
+ ].freeze
16
+
17
+ OPTION_NAMES = ORDERED_OPTIONS.map(&:key)
18
+
19
+ attr_reader :options
20
+
21
+ def dup = OptionsSet.new(options.deep_dup)
22
+ def preload? = options[:loading] == :preload
23
+ def custom_animation? = options[:animation].to_s == "custom"
24
+ def to_h = options
25
+
26
+ def initialize(options = {})
27
+ @options = options
28
+ end
29
+
30
+ def merge(options_set)
31
+ OptionsSet.new(@options.merge(options_set.options))
32
+ end
33
+
34
+ def validate!
35
+ ORDERED_OPTIONS.map do |option|
36
+ next unless @options.key?(option.key)
37
+
38
+ value = @options[option.key]
39
+ option.new(value).validate!
40
+ end
41
+ @options.assert_valid_keys(ORDERED_OPTIONS.map(&:key))
42
+ end
43
+
44
+ def to_base36
45
+ @to_base36 ||= begin
46
+ shift = 0
47
+ ORDERED_OPTIONS.reverse.sum do |option|
48
+ bits = option.into_bits(@options[option.key])
49
+ result = bits << shift
50
+ shift += option.bit_size
51
+ result
52
+ end.to_s(36).freeze
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Registry
6
+ attr_reader :registry, :semaphore
7
+
8
+ def initialize
9
+ @semaphore = Mutex.new
10
+ @registry = Hash.new
11
+ end
12
+
13
+ def register(type, klass) = @semaphore.synchronize { @registry[type] = klass }
14
+ def lookup(type) = @semaphore.synchronize {@registry.fetch(type) }
15
+ def safe_lookup(type) = @semaphore.synchronize {@registry[type] }
16
+
17
+ def lookup_or_register(type)
18
+ safe_lookup(type) ||
19
+ begin
20
+ register(type, (type.classify + "Hovercard").constantize)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Setup
6
+ attr_reader :klass, :type, :params
7
+
8
+ EMPTY_PARAMS = {}.freeze
9
+
10
+ def initialize(klass)
11
+ @klass = klass
12
+ @type = nil
13
+ @params = EMPTY_PARAMS
14
+ end
15
+
16
+ def default_options = klass.default_options_for(type)
17
+ def identifier = "#{type}@#{klass.hovercard_resource_name}"
18
+ def render_in(view_context) = klass.new(params, view_context).process(type)
19
+
20
+ def with_type(type)
21
+ @type = type
22
+ self
23
+ end
24
+
25
+ def with_params(**params)
26
+ @params = params
27
+ self
28
+ end
29
+
30
+ def method_missing(method_name, *args, &)
31
+ if klass.action_methods.include?(method_name.name)
32
+ raise ArgumentError, "expected no arguments, given #{args.size}" if args.any?
33
+ with_type(method_name)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method, include_all = false)
40
+ klass.action_methods.include?(method.name) || super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ module ViewContextDelegation
6
+ attr_accessor :__cp_view_context
7
+
8
+ # For CSRF authenticity tokens in forms
9
+ def config = __cp_view_context.config
10
+ def form_authenticity_token(...) = __cp_view_context.form_authenticity_token(...)
11
+ def protect_against_forgery? = __cp_view_context.protect_against_forgery?
12
+ def request_forgery_protection_token = __cp_view_context.request_forgery_protection_token
13
+
14
+ def helpers = __cp_view_context
15
+ def controller = __cp_view_context.controller
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard < AbstractController::Base
5
+ abstract!
6
+
7
+ include AbstractController::Rendering
8
+
9
+ include AbstractController::Logger
10
+ include AbstractController::Helpers
11
+ include AbstractController::Translation
12
+ include AbstractController::Callbacks
13
+ include AbstractController::Caching
14
+
15
+ include ActionView::Layouts
16
+ include ActionView::Rendering
17
+
18
+ include Rails.application.routes.url_helpers
19
+
20
+ layout "hovercard"
21
+
22
+ # so coupdoeil helpers are always available within hovercards
23
+ helper Coupdoeil::ApplicationHelper
24
+
25
+ @registry = Registry.new
26
+ @default_options_by_method = {}.with_indifferent_access
27
+
28
+ DEFAULT_OPTIONS_KEY = Object.new
29
+ private_constant :DEFAULT_OPTIONS_KEY
30
+
31
+ @default_options_by_method[DEFAULT_OPTIONS_KEY] = OptionsSet.new(
32
+ offset: 0,
33
+ placement: "auto",
34
+ animation: "slide-in",
35
+ cache: true,
36
+ loading: :async,
37
+ trigger: "hover"
38
+ )
39
+
40
+ DoubleRenderError = Class.new(::AbstractController::DoubleRenderError)
41
+
42
+ # See engine initialization for view paths
43
+
44
+ class << self
45
+ attr_reader :registry
46
+
47
+ def hovercard_resource_name = @hovercard_resource_name ||= name.delete_suffix("Hovercard").underscore
48
+ def with(...) = Setup.new(self).with_params(...)
49
+
50
+ def inherited(subclass)
51
+ super
52
+ Coupdoeil::Hovercard.registry.register(subclass.hovercard_resource_name, subclass)
53
+ subclass.instance_variable_set(:@default_options_by_method, @default_options_by_method.dup)
54
+ end
55
+
56
+ def default_options(...) = default_options_for(DEFAULT_OPTIONS_KEY, ...)
57
+
58
+ def default_options_for(*action_names, **option_values)
59
+ if option_values.blank?
60
+ @default_options_by_method[action_names.first] || default_options
61
+ else
62
+ action_names.each do |action_name|
63
+ options = @default_options_by_method[action_name] || default_options
64
+ @default_options_by_method[action_name] = options.merge(OptionsSet.new(option_values))
65
+ end
66
+ end
67
+ end
68
+
69
+ def method_missing(method_name, *args, &)
70
+ return super unless action_methods.include?(method_name.name)
71
+ raise ArgumentError, "expected no arguments" if args.any?
72
+
73
+ Setup.new(self).with_type(method_name)
74
+ end
75
+
76
+ def respond_to_missing?(method, include_all = false)
77
+ action_methods.include?(method.name) || super
78
+ end
79
+ end
80
+
81
+ attr_reader :params
82
+
83
+ def initialize(params, cp_view_context)
84
+ super()
85
+ @params = params
86
+ @__cp_view_context = cp_view_context
87
+ end
88
+
89
+ def helpers = @__cp_view_context
90
+ def controller = @__cp_view_context.controller
91
+
92
+ def view_context
93
+ super.tap do |context|
94
+ context.extend ViewContextDelegation
95
+ context.__cp_view_context = @__cp_view_context
96
+ end
97
+ end
98
+
99
+ def render(...)
100
+ return super unless response_body
101
+
102
+ raise DoubleRenderError, "Render was called multiple times in this action. \
103
+ Also note that render does not terminate execution of the action."
104
+ end
105
+
106
+ def process(method_name, *)
107
+ benchmark("processed hovercard #{self.class.hovercard_resource_name}/#{method_name}", silence: true) do
108
+ ActiveSupport::Notifications.instrument("render_hovercard.coupdoeil") do
109
+ super
110
+ response_body || render(action_name)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end