coupdoeil 1.0.0.pre.beta.2 → 1.0.0.pre.beta.3

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.
@@ -2,15 +2,8 @@
2
2
 
3
3
  module Coupdoeil
4
4
  module ApplicationHelper
5
- def coupdoeil_popover_tag(popover, options = nil, **attributes_or_options, &)
6
- if options.present?
7
- attributes = attributes_or_options
8
- else
9
- options = attributes_or_options.extract!(*Popover::OptionsSet::OPTION_NAMES)
10
- attributes = attributes_or_options
11
- end
12
- popover_options = options
13
- render(Coupdoeil::Tag.new(popover:, popover_options:, attributes:), &)
5
+ def coupdoeil_popover_tag(popover, popover_options = nil, tag_attributes = nil, &)
6
+ render(Coupdoeil::Tag.new(popover:, popover_options:, attributes: tag_attributes), &)
14
7
  end
15
8
  end
16
9
  end
@@ -1,4 +1,4 @@
1
- import {extractOptionFromElement} from "./optionsParser";
1
+ import {extractOptionFromElement} from "./options_parser";
2
2
 
3
3
  export function getType(controller) {
4
4
  return controller.coupdoeilElement.getAttribute('popover-type')
@@ -1,7 +1,7 @@
1
1
  import {FETCH_DELAY_MS, POPOVER_CLASS_NAME, OPENING_DELAY_MS} from "./config"
2
2
  import {getParams, getType, preloadedContentElement, triggeredOnClick} from "./attributes"
3
3
  import {getPopoverContentHTML, setPopoverContentHTML} from "./cache"
4
- import {extractOptionsFromElement} from "./optionsParser"
4
+ import {extractOptionsFromElement} from "./options_parser"
5
5
  import {positionPopover} from "./positioning"
6
6
  import {enter} from "el-transition"
7
7
  import {addToCurrents} from "./current"
@@ -15,7 +15,7 @@ const ORDERED_OPTIONS = [
15
15
  "openingDelay", // bit size: 1 shift: 4
16
16
  "animation", // bit size: 3 shift: 5
17
17
  "placement", // bit size: 16 shift: 8
18
- "offset" // bit size: 21 shift: 24
18
+ "offset" // bit size: 20 shift: 24
19
19
  ]
20
20
 
21
21
  const TRIGGERS = ["hover", "click"]
@@ -40,6 +40,13 @@ function parseCSSSize(value) {
40
40
  return 0
41
41
  }
42
42
 
43
+ // Offset option is a three part number.
44
+ // integer (8 bits) + decimals (10 bits) + config (2 bits)
45
+ // config bits are for sign (positive (0) or negative (1)) and offset unit (px (0) or rem (1))
46
+ // Examples:
47
+ // - "1.25rem" is: 00000001 . 0000011001 . 10
48
+ // - "-16px" is: 00010000 . 00000000 . 01
49
+ // 10 bits are reserved for decimals so values like 0.875rem are possible.
43
50
  function getOffset(optionsInt) {
44
51
  // shift is BigInt(16 + 3 + 1 + 1 + 2 + 1)
45
52
  const offsetBits = Number(BigInt(optionsInt) >> BigInt(24))
@@ -48,16 +55,21 @@ function getOffset(optionsInt) {
48
55
 
49
56
  const isNegative = (offsetBits & 1) === 1
50
57
  const isREM = (offsetBits & 2) === 2
51
- // decimals mask is 2 ** 11 - 1
52
- const decimals = (offsetBits >> 2) & 2047
53
- const integer = (offsetBits >> (2 + 11))
58
+ const decimals = (offsetBits >> 2 /* config bits */) & 1023 // (2 ** 10) - 1
59
+ const integer = (offsetBits >> (12 /* config (2) + decimals (10) bits */))
54
60
 
55
61
  const CSSSize = `${isNegative ? '-' : ''}${integer}.${decimals}${isREM ? 'rem' : 'px'}`
56
62
  return parseCSSSize(CSSSize)
57
63
  }
58
64
 
65
+ // Placement option can have up to 4 placement values: a main one and 3 fallbacks.
66
+ // There are 13 possible values for one placement, stored as an array in the PLACEMENTS const.
67
+ // The max index of this array is 12, so a placement value can be stored as an index of this array,
68
+ // and this index value fits in 4 bits.
69
+ // Option for placements then consists of 4 times (main and fallbacks) this index value:
70
+ // So from 0.0.0.0 up to 15.15.15.15, or from 0 up to 65535 (0b1111111111111111)
59
71
  function getPlacement(optionsInt) {
60
- // shift is 3 + 1 + 1 + 2 + 1, mask is 2 ** 16 - 1
72
+ // shift is 3 + 1 + 1 + 2 + 1, mask is 2 ** 16 - 1 or 0b1111111111111111
61
73
  const placementBits = (optionsInt >> 8) & 65535
62
74
  let shift = 0
63
75
  let lastPlacement = null
@@ -4,7 +4,10 @@ module Coupdoeil
4
4
  class Popover
5
5
  class Option
6
6
  class Offset < Coupdoeil::Popover::Option
7
- self.bit_size = 8 + 11 + 2
7
+ INTEGER_PART_BITS = 8
8
+ FLOAT_PART_BITS = 10
9
+ CONFIG_PART_BITS = 2
10
+ self.bit_size = INTEGER_PART_BITS + FLOAT_PART_BITS + CONFIG_PART_BITS
8
11
 
9
12
  class << self
10
13
  def parse(value)
@@ -16,19 +19,29 @@ module Coupdoeil
16
19
  base |= 2 if value.is_a?(String) && value.end_with?("rem")
17
20
 
18
21
  integer, decimals = float_value.abs.to_s.split(".")
19
- base |= (decimals.to_i << 2)
20
- base |= (integer.to_i << 2 + 11)
22
+ base |= (decimals.to_i << CONFIG_PART_BITS)
23
+ base |= (integer.to_i << CONFIG_PART_BITS + FLOAT_PART_BITS)
21
24
 
22
25
  base
23
26
  end
24
27
  end
25
28
 
26
29
  def validate!
27
- return if value in Float | Integer
28
- return if value.to_s.match?(/^-?\d+(\.\d{1,3})?(px|rem)?$/)
30
+ return ensure_no_overflow if (value in Float | Integer) || value.to_s.match?(/^-?\d+(\.\d{1,3})?(px|rem)?$/)
29
31
 
30
32
  raise_invalid_option "Value should be a signed float or integer, followed or not by 'rem' or 'px'."
31
33
  end
34
+
35
+ private
36
+
37
+ def ensure_no_overflow
38
+ float_value = value.to_f
39
+ integer, decimals = float_value.abs.to_s.split(".").map(&:to_i)
40
+ return if integer.in?(0..255) && decimals.in?(0..999)
41
+
42
+ raise_invalid_option "Number should be comprised between -255.999 and 255.999, \
43
+ with a maximum of 3 decimal digits."
44
+ end
32
45
  end
33
46
  end
34
47
  end
@@ -23,28 +23,33 @@ module Coupdoeil
23
23
  def to_h = options
24
24
 
25
25
  def initialize(options = {})
26
+ options.assert_valid_keys(OPTION_NAMES)
27
+
26
28
  @options = options
27
29
  end
28
30
 
29
31
  def merge(options_set)
30
- OptionsSet.new(@options.merge(options_set.options))
32
+ OptionsSet.new(options.merge(options_set.options))
31
33
  end
32
34
 
33
- def validate!
34
- ORDERED_OPTIONS.map do |option|
35
- next unless @options.key?(option.key)
35
+ if Rails.env.local?
36
+ def validate!
37
+ ORDERED_OPTIONS.map do |option|
38
+ next unless options.key?(option.key)
36
39
 
37
- value = @options[option.key]
38
- option.new(value).validate!
40
+ value = options[option.key]
41
+ option.new(value).validate!
42
+ end
39
43
  end
40
- @options.assert_valid_keys(ORDERED_OPTIONS.map(&:key))
44
+ else
45
+ def validate! = nil # no-op
41
46
  end
42
47
 
43
48
  def to_base36
44
49
  @to_base36 ||= begin
45
50
  shift = 0
46
51
  ORDERED_OPTIONS.reverse.sum do |option|
47
- bits = option.into_bits(@options[option.key])
52
+ bits = option.into_bits(options[option.key])
48
53
  result = bits << shift
49
54
  shift += option.bit_size
50
55
  result
@@ -13,9 +13,9 @@ module Coupdoeil
13
13
  @params = EMPTY_PARAMS
14
14
  end
15
15
 
16
- def default_options = klass.default_options_for(type)
17
16
  def identifier = "#{type}@#{klass.popover_resource_name}"
18
17
  def render_in(view_context) = klass.new(params, view_context).process(type)
18
+ def options = @options ||= klass.default_options_for(type)
19
19
 
20
20
  def with_type(type)
21
21
  @type = type
@@ -27,17 +27,9 @@ module Coupdoeil
27
27
  self
28
28
  end
29
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
30
+ def with_options(new_options)
31
+ @options = options.merge(Popover::OptionsSet.new(new_options))
32
+ self
41
33
  end
42
34
  end
43
35
  end
@@ -46,7 +46,7 @@ module Coupdoeil
46
46
  attr_reader :registry
47
47
 
48
48
  def popover_resource_name = @popover_resource_name ||= name.delete_suffix("Popover").underscore
49
- def with(...) = Setup.new(self).with_params(...)
49
+ def with(...) = setup_class.new(self).with_params(...)
50
50
 
51
51
  def inherited(subclass)
52
52
  super
@@ -69,14 +69,26 @@ module Coupdoeil
69
69
 
70
70
  def method_missing(method_name, *args, &)
71
71
  return super unless action_methods.include?(method_name.name)
72
- raise ArgumentError, "expected no arguments" if args.any?
73
72
 
74
- Setup.new(self).with_type(method_name)
75
- end
73
+ action_methods.each do |action_name|
74
+ define_singleton_method(action_name) { setup_class.new(self).with_type(action_name) }
75
+ end
76
+ public_send(method_name)
77
+ end
76
78
 
77
79
  def respond_to_missing?(method, include_all = false)
78
80
  action_methods.include?(method.name) || super
79
81
  end
82
+
83
+ def setup_class
84
+ @setup_class ||= begin
85
+ setup_klass = Class.new(Setup)
86
+ action_methods.each do |action_name|
87
+ setup_klass.define_method(action_name) { with_type(action_name) }
88
+ end
89
+ setup_klass
90
+ end
91
+ end
80
92
  end
81
93
 
82
94
  attr_reader :params
@@ -2,19 +2,20 @@
2
2
 
3
3
  module Coupdoeil
4
4
  class Tag
5
+ delegate :options, to: :popover_setup, prefix: :popover
6
+
5
7
  def initialize(popover:, popover_options:, attributes:)
6
8
  @popover_setup = popover
9
+ @popover_setup.with_options(popover_options) if popover_options
7
10
  @attributes = attributes
8
- popover_options = Popover::OptionsSet.new(popover_options)
9
- @popover_options_set = popover_setup.default_options.merge(popover_options)
10
- @popover_options_set.validate!
11
+ @popover_setup.options.validate!
11
12
  end
12
13
 
13
14
  def render_in(view_context, &block)
14
15
  ActiveSupport::Notifications.instrument("render_tag.coupdoeil") do
15
16
  content = view_context.capture(&block) if block_given?
16
- view_context.content_tag("coup-doeil", **@attributes.merge(popover_attributes)) do
17
- if popover_options_set.preload?
17
+ view_context.content_tag("coup-doeil", **tag_attributes) do
18
+ if popover_options.preload?
18
19
  view_context.tag.template(view_context.render(popover_setup), class: "popover-content") + content
19
20
  else
20
21
  content
@@ -25,21 +26,29 @@ module Coupdoeil
25
26
 
26
27
  private
27
28
 
28
- attr_reader :popover_options_set, :popover_setup
29
+ attr_reader :popover_setup
29
30
 
30
31
  def popover_attributes
31
- attributes = { "popover-options": popover_options_set.to_base36 }
32
+ attributes = { "popover-options": popover_options.to_base36 }
32
33
 
33
- unless popover_options_set.preload?
34
+ unless popover_options.preload?
34
35
  params = Params.serialize(popover_setup.params).sole.presence&.to_json
35
36
  attributes.merge!("popover-type" => popover_setup.identifier, "popover-params" => params)
36
37
  end
37
38
 
38
39
  if Rails.env.local?
39
- attributes.merge!(popover_options_set.to_h.transform_keys { "popover-#{_1}" })
40
+ attributes.merge!(popover_options.to_h.transform_keys { "popover-#{_1}" })
40
41
  end
41
42
 
42
43
  attributes
43
44
  end
45
+
46
+ def tag_attributes
47
+ if @attributes
48
+ @attributes.merge(popover_attributes)
49
+ else
50
+ popover_attributes
51
+ end
52
+ end
44
53
  end
45
54
  end
@@ -1,3 +1,3 @@
1
1
  module Coupdoeil
2
- VERSION = "1.0.0-beta.2"
2
+ VERSION = "1.0.0-beta.3"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coupdoeil
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.2
4
+ version: 1.0.0.pre.beta.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - PageHey
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-06-13 00:00:00.000000000 Z
10
+ date: 2025-06-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionpack
@@ -162,7 +162,7 @@ files:
162
162
  - app/javascript/coupdoeil/popover/controller.js
163
163
  - app/javascript/coupdoeil/popover/current.js
164
164
  - app/javascript/coupdoeil/popover/opening.js
165
- - app/javascript/coupdoeil/popover/optionsParser.js
165
+ - app/javascript/coupdoeil/popover/options_parser.js
166
166
  - app/javascript/coupdoeil/popover/positioning.js
167
167
  - app/javascript/coupdoeil/popover/state_check.js
168
168
  - app/models/coupdoeil/params.rb
@@ -200,7 +200,7 @@ licenses:
200
200
  metadata:
201
201
  homepage_uri: https://coupdoeil.org
202
202
  source_code_uri: https://gitlab.com/Pagehey/coupdoeil
203
- changelog_uri: https://gitlab.com/Pagehey/coupdoeil/CHANGELOG.md
203
+ changelog_uri: https://gitlab.com/Pagehey/coupdoeil/-/blob/main/CHANGELOG.md
204
204
  rdoc_options: []
205
205
  require_paths:
206
206
  - lib