coupdoeil 1.0.0.pre.alpha.9 → 1.0.0.pre.alpha.10

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.
@@ -17,17 +17,29 @@ export default class extends HTMLElement {
17
17
  this.hovercardController = new HovercardController(this)
18
18
  }
19
19
 
20
- openHovercard() {
21
- if (this.openingHovercard || this.hovercardController.isOpen) return;
20
+ openHovercard(triggerElement = null, callbacks) {
21
+ if (this.openingHovercard || this.hovercardController.isOpen || this.disabled || triggerElement === this) return;
22
22
 
23
23
  this.openingHovercard = true
24
24
 
25
25
  const parent = this.closest(HOVERCARD_SELECTOR)?.controller
26
26
  addToCurrents(this)
27
- return openHovercard(this.hovercardController, { parent })
27
+ return openHovercard(this.hovercardController, { parent, ...callbacks })
28
28
  }
29
29
 
30
30
  closeHovercard() {
31
31
  closeNow(this.hovercardController)
32
32
  }
33
+
34
+ get disabled() {
35
+ return !!this.getAttribute("disabled")
36
+ }
37
+
38
+ set disabled(disabled) {
39
+ if (disabled) {
40
+ this.setAttribute("disabled", true)
41
+ } else {
42
+ this.removeAttribute("disabled")
43
+ }
44
+ }
33
45
  }
@@ -8,10 +8,10 @@ export const coupdoeilOnClickEvent = ({ target: clickedElement }) => {
8
8
  const hovercardElement = clickedElement.closest(HOVERCARD_SELECTOR)
9
9
 
10
10
  if (coupdoeilElement && hovercardElement) {
11
- handleClickedCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement)
11
+ handleClickedCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement, clickedElement)
12
12
  }
13
13
  else if (coupdoeilElement) {
14
- handleClickedCoupdoeilOutsideHovercard(coupdoeilElement)
14
+ handleClickedCoupdoeilOutsideHovercard(coupdoeilElement, clickedElement)
15
15
  }
16
16
  else if (hovercardElement) {
17
17
  handleClickOutsideCoupdoeilButWithinHovercard(hovercardElement, clickedElement)
@@ -21,7 +21,7 @@ export const coupdoeilOnClickEvent = ({ target: clickedElement }) => {
21
21
  }
22
22
  }
23
23
 
24
- function handleClickedCoupdoeilWithinHovercard(coupdoeilElement, _hovercardElement) {
24
+ function handleClickedCoupdoeilWithinHovercard(coupdoeilElement, _hovercardElement, clickedElement) {
25
25
  const hovercard = coupdoeilElement.hovercardController
26
26
  if(noTriggeredOnClick(hovercard))
27
27
  return;
@@ -32,11 +32,11 @@ function handleClickedCoupdoeilWithinHovercard(coupdoeilElement, _hovercardEleme
32
32
  } else {
33
33
  // first click on a closed hovercard trigger opens it
34
34
  // If any other hovercard is open, it is the parent hovercard, hence it should not be closed.
35
- coupdoeilElement.openHovercard()
35
+ coupdoeilElement.openHovercard(clickedElement)
36
36
  }
37
37
  }
38
38
 
39
- function handleClickedCoupdoeilOutsideHovercard(coupdoeilElement) {
39
+ function handleClickedCoupdoeilOutsideHovercard(coupdoeilElement, clickedElement) {
40
40
  const hovercard = coupdoeilElement.hovercardController
41
41
  if(noTriggeredOnClick(hovercard))
42
42
  return;
@@ -48,7 +48,7 @@ function handleClickedCoupdoeilOutsideHovercard(coupdoeilElement) {
48
48
  // close any other open hovercard
49
49
  closeAllNow()
50
50
  // first click on a closed hovercard trigger opens it
51
- coupdoeilElement.openHovercard()
51
+ coupdoeilElement.openHovercard(clickedElement)
52
52
  }
53
53
  }
54
54
 
@@ -5,7 +5,8 @@ import {
5
5
  cancelCloseRequest,
6
6
  closeChildrenNow,
7
7
  closeOnHoverChildrenLater,
8
- closeTriggeredOnHoverLater, closeTriggeredOnHoverNow
8
+ closeTriggeredOnHoverLater,
9
+ closeTriggeredOnHoverNowUnlessAncestor
9
10
  } from "../hovercard/closing";
10
11
  import {addToCurrents as addToCurrentHovercards} from "../hovercard/current";
11
12
 
@@ -14,10 +15,10 @@ export const onMouseOver = ({ target: hoveredElement }) => {
14
15
  const hovercardElement = hoveredElement.closest(HOVERCARD_SELECTOR)
15
16
 
16
17
  if (coupdoeilElement && hovercardElement) {
17
- handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement)
18
+ handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement, hoveredElement)
18
19
  }
19
20
  else if (coupdoeilElement) {
20
- handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement)
21
+ handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement, hoveredElement)
21
22
  }
22
23
  else if (hovercardElement) {
23
24
  handleOverOutsideCoupdoeilButWithinHovercard(hovercardElement)
@@ -27,7 +28,7 @@ export const onMouseOver = ({ target: hoveredElement }) => {
27
28
  }
28
29
  }
29
30
 
30
- function handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement) {
31
+ function handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement, hoveredElement) {
31
32
  const childHovercard = coupdoeilElement.hovercardController
32
33
  const parentHovercard = hovercardElement.controller
33
34
  if(notTriggeredOnHover(childHovercard))
@@ -41,19 +42,18 @@ function handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElem
41
42
  // ensures to close other children hovercards before opening the one that current one
42
43
  closeChildrenNow(parentHovercard)
43
44
  // should also close any open hovercard outside of parent
44
- coupdoeilElement.openHovercard()
45
+ coupdoeilElement.openHovercard(hoveredElement)
45
46
  }
46
47
  }
47
48
 
48
- function handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement) {
49
+ function handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement, hoveredElement) {
49
50
  const hovercard = coupdoeilElement.hovercardController
50
51
  if(notTriggeredOnHover(hovercard))
51
52
  return;
52
53
 
53
54
  if (hovercard.isClosed) {
54
55
  // Close any other open hovercard before opening this one
55
- closeTriggeredOnHoverNow()
56
- coupdoeilElement.openHovercard()
56
+ coupdoeilElement.openHovercard(hoveredElement, { beforeDisplay: closeTriggeredOnHoverNowUnlessAncestor })
57
57
  } else if (hovercard.closingRequest) {
58
58
  // hovercard is still open but was requested to close, then it clear this closing request
59
59
  // and ensures the hovercards stays in current hovercards register
@@ -1,5 +1,5 @@
1
1
  import {triggeredOnHover} from "./attributes"
2
- import {defaultConfig} from "./config"
2
+ import {CLOSING_DELAY_MS} from "./config"
3
3
  import {leave} from "el-transition"
4
4
  import {addToCurrents, CURRENT_HOVERCARDS_BY_ID, removeFromCurrents} from "./current"
5
5
 
@@ -68,7 +68,7 @@ export function closeLater(controller) {
68
68
  cancelOpenCloseActions(controller)
69
69
  controller.closingRequest = setTimeout(() => {
70
70
  closeNow(controller)
71
- }, defaultConfig.closingDelay)
71
+ }, CLOSING_DELAY_MS)
72
72
  }
73
73
 
74
74
  export function closeChildrenNow(controller) {
@@ -123,3 +123,18 @@ export function closeTriggeredOnHoverLater() {
123
123
  }
124
124
  }
125
125
  }
126
+
127
+ export function closeTriggeredOnHoverNowUnlessAncestor(controller) {
128
+ let topMostParent = controller
129
+ while (topMostParent.parent) {
130
+ topMostParent = topMostParent.parent
131
+ }
132
+ const idToSkip = topMostParent.coupdoeilElement.uniqueId
133
+
134
+ for (const coupdoeilElement of CURRENT_HOVERCARDS_BY_ID.values()) {
135
+ if (coupdoeilElement.uniqueId !== idToSkip && triggeredOnHover(coupdoeilElement.hovercardController)) {
136
+ closeNow(coupdoeilElement.hovercardController)
137
+ removeFromCurrents(coupdoeilElement)
138
+ }
139
+ }
140
+ }
@@ -2,14 +2,14 @@ export const HOVERCARD_CLASS_NAME = 'coupdoeil--hovercard'
2
2
  export const HOVERCARD_SELECTOR = `.${HOVERCARD_CLASS_NAME}`
3
3
  export const HOVERCARD_CLOSE_BTN_SELECTOR = '[data-hovercard-close]'
4
4
 
5
- export const defaultConfig = {
6
- // the time (ms) to wait before closing the hovercard,
7
- // to avoid flickering if the user hovers out and in quickly,
8
- // or if the user moves the mouse from the target to the hovercard
9
- closingDelay: 150,
10
- // the time (ms) to wait before starting to fetch the content,
11
- // to avoid fetching too soon if the user hovers in and out the target quickly
12
- fetchDelay: 100,
13
- // the minimum time (ms) the user should wait before seeing the hovercard
14
- openingDelay: 200,
15
- }
5
+ // the time (ms) to wait before closing the hovercard,
6
+ // to avoid flickering if the user hovers out and in quickly,
7
+ // or if the user moves the mouse from the target to the hovercard
8
+ export const CLOSING_DELAY_MS = 75
9
+
10
+ // the time (ms) to wait before starting to fetch the content,
11
+ // to avoid fetching too soon if the user hovers in and out the target quickly
12
+ export const FETCH_DELAY_MS = 100
13
+
14
+ // the minimum time (ms) the user should wait before seeing the hovercard
15
+ export const OPENING_DELAY_MS = 200
@@ -1,4 +1,4 @@
1
- import {defaultConfig, HOVERCARD_CLASS_NAME} from "./config"
1
+ import {FETCH_DELAY_MS, HOVERCARD_CLASS_NAME, OPENING_DELAY_MS} from "./config"
2
2
  import {getParams, getType, preloadedContentElement, triggeredOnClick} from "./attributes"
3
3
  import {getHovercardContentHTML, setHovercardContentHTML} from "./cache"
4
4
  import {extractOptionsFromElement} from "./optionsParser"
@@ -47,7 +47,7 @@ async function loadHovercardContentHTML(controller, options, delayOptions) {
47
47
  })
48
48
  }
49
49
 
50
- export async function openHovercard(controller, { parent }) {
50
+ export async function openHovercard(controller, { parent, beforeDisplay }) {
51
51
  if (controller.isOpen) {
52
52
  return cancelCloseRequest(controller)
53
53
  }
@@ -56,8 +56,8 @@ export async function openHovercard(controller, { parent }) {
56
56
  parent.children.add(controller)
57
57
  }
58
58
 
59
- const delays = getDelayOptionsForController(controller)
60
59
  const options = extractOptionsFromElement(controller.coupdoeilElement)
60
+ const delays = getDelayOptionsForController(controller, options)
61
61
 
62
62
  const openingDelay = new Promise(resolve => setTimeout(resolve, delays.opening))
63
63
  const fetchDelay = loadHovercardContentHTML(controller, options, delays)
@@ -67,11 +67,11 @@ export async function openHovercard(controller, { parent }) {
67
67
 
68
68
  // but if opening has been canceled (nullified), the wait still happens, so we need to check again
69
69
  if (controller.coupdoeilElement.openingHovercard && !parentIsClosedOrClosing) {
70
- await display(controller, options)
70
+ await display(controller, options, beforeDisplay)
71
71
  }
72
72
  }
73
73
 
74
- async function display(controller, options) {
74
+ async function display(controller, options, beforeDisplay) {
75
75
  if (controller.isOpen) return;
76
76
 
77
77
  cancelCloseRequest(controller)
@@ -91,6 +91,9 @@ async function display(controller, options) {
91
91
  controller.card.style.removeProperty('visibility')
92
92
 
93
93
  executeNextFrameIfStillOpening(controller, async () => {
94
+ if (beforeDisplay) {
95
+ beforeDisplay(controller)
96
+ }
94
97
  // // adding again the card to make sure it is in the map, could be better
95
98
  addToCurrents(controller.coupdoeilElement)
96
99
  delete controller.coupdoeilElement.openingHovercard
@@ -111,18 +114,12 @@ function executeNextFrameIfStillOpening(controller, callback) {
111
114
  })
112
115
  }
113
116
 
114
- function getDelayOptionsForController(controller) {
115
- if (triggeredOnClick(controller)) {
117
+ function getDelayOptionsForController(controller, options) {
118
+ if (options.openingDelay === false || triggeredOnClick(controller)) {
116
119
  return { fetch: 0, opening: 0 }
117
120
  }
118
121
 
119
- let fetchDelay
120
- if (defaultConfig.openingDelay === 0) {
121
- fetchDelay = 0
122
- } else {
123
- fetchDelay = defaultConfig.openingDelay / 2
124
- }
125
- return { fetch: fetchDelay, opening: defaultConfig.openingDelay }
122
+ return { fetch: FETCH_DELAY_MS, opening: OPENING_DELAY_MS }
126
123
  }
127
124
 
128
125
  function buildHovercardElement(controller, options) {
@@ -1,19 +1,21 @@
1
1
  const OPTIONS = {
2
2
  animation: { getter: getAnimation },
3
3
  cache: { getter: getCache },
4
+ loading: { getter: getLoading },
4
5
  offset: { getter: getOffset },
6
+ openingDelay: { getter: getOpeningDelay },
5
7
  placement: { getter: getPlacement },
6
- loading: { getter: getLoading },
7
- trigger: { getter: getTrigger }
8
+ trigger: { getter: getTrigger },
8
9
  }
9
10
 
10
11
  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
12
+ "trigger", // bit size: 1 shift: 0
13
+ "loading", // bit size: 2 shift: 1
14
+ "cache", // bit size: 1 shift: 3
15
+ "openingDelay", // bit size: 1 shift: 4
16
+ "animation", // bit size: 3 shift: 5
17
+ "placement", // bit size: 16 shift: 8
18
+ "offset" // bit size: 21 shift: 24
17
19
  ]
18
20
 
19
21
  const TRIGGERS = ["hover", "click"]
@@ -39,8 +41,8 @@ function parseCSSSize(value) {
39
41
  }
40
42
 
41
43
  function getOffset(optionsInt) {
42
- // shift is BigInt(16 + 3 + 1 + 2 + 1)
43
- const offsetBits = Number(BigInt(optionsInt) >> BigInt(23))
44
+ // shift is BigInt(16 + 3 + 1 + 1 + 2 + 1)
45
+ const offsetBits = Number(BigInt(optionsInt) >> BigInt(24))
44
46
  if (offsetBits === 0)
45
47
  return 0
46
48
 
@@ -55,8 +57,8 @@ function getOffset(optionsInt) {
55
57
  }
56
58
 
57
59
  function getPlacement(optionsInt) {
58
- // shift is 3 + 1 + 2 + 1, mask is 2 ** 16 - 1
59
- const placementBits = (optionsInt >> 7) & 65535
60
+ // shift is 3 + 1 + 1 + 2 + 1, mask is 2 ** 16 - 1
61
+ const placementBits = (optionsInt >> 8) & 65535
60
62
  let shift = 0
61
63
  let lastPlacement = null
62
64
  const placements = []
@@ -70,7 +72,12 @@ function getPlacement(optionsInt) {
70
72
  }
71
73
 
72
74
  function getAnimation(optionsInt) {
73
- return ANIMATIONS[(optionsInt & 56) >> 4]
75
+ // return ANIMATIONS[(optionsInt & 56) >> 5]
76
+ return ANIMATIONS[(optionsInt >> 5) & 7]
77
+ }
78
+
79
+ function getOpeningDelay(optionsInt) {
80
+ return ((optionsInt >> 4) & 1) === 1
74
81
  }
75
82
 
76
83
  function getCache(optionsInt) {
@@ -89,9 +96,10 @@ function getTrigger(optionsInt) {
89
96
  const HovercardOptions = {
90
97
  animation: undefined,
91
98
  cache: undefined,
99
+ loading: undefined,
92
100
  offset: undefined,
101
+ openingDelay: undefined,
93
102
  placement: undefined,
94
- loading: undefined,
95
103
  trigger: undefined,
96
104
  }
97
105
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Hovercard
5
+ class Option
6
+ class OpeningDelay < 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
@@ -7,6 +7,7 @@ module Coupdoeil
7
7
  Option::Offset,
8
8
  Option::Placement,
9
9
  Option::Animation,
10
+ Option::OpeningDelay,
10
11
  Option::Cache,
11
12
  Option::Loading,
12
13
  Option::Trigger
@@ -3,7 +3,7 @@
3
3
  module Coupdoeil
4
4
  class Hovercard
5
5
  module ViewContextDelegation
6
- attr_accessor :__cp_view_context
6
+ attr_accessor :__cp_view_context, :hovercard
7
7
 
8
8
  # For CSRF authenticity tokens in forms
9
9
  def config = __cp_view_context.config
@@ -13,6 +13,7 @@ module Coupdoeil
13
13
 
14
14
  def helpers = __cp_view_context
15
15
  def controller = __cp_view_context.controller
16
+ def params = hovercard.params
16
17
  end
17
18
  end
18
19
  end
@@ -34,7 +34,8 @@ module Coupdoeil
34
34
  animation: "slide-in",
35
35
  cache: true,
36
36
  loading: :async,
37
- trigger: "hover"
37
+ trigger: "hover",
38
+ opening_delay: true
38
39
  )
39
40
 
40
41
  DoubleRenderError = Class.new(::AbstractController::DoubleRenderError)
@@ -92,6 +93,7 @@ module Coupdoeil
92
93
  def view_context
93
94
  super.tap do |context|
94
95
  context.extend ViewContextDelegation
96
+ context.hovercard = self
95
97
  context.__cp_view_context = @__cp_view_context
96
98
  end
97
99
  end
@@ -1,3 +1,3 @@
1
1
  module Coupdoeil
2
- VERSION = "1.0.0-alpha.9"
2
+ VERSION = "1.0.0-alpha.10"
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.alpha.9
4
+ version: 1.0.0.pre.alpha.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - PageHey
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-24 00:00:00.000000000 Z
10
+ date: 2025-05-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionpack
@@ -99,6 +99,34 @@ dependencies:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: turbo-rails
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ - !ruby/object:Gem::Dependency
117
+ name: stimulus-rails
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
102
130
  description: Easy and powerful hovercard system for Ruby On Rails.
103
131
  email:
104
132
  - pagehey@pm.me
@@ -143,6 +171,7 @@ files:
143
171
  - app/models/coupdoeil/hovercard/option/cache.rb
144
172
  - app/models/coupdoeil/hovercard/option/loading.rb
145
173
  - app/models/coupdoeil/hovercard/option/offset.rb
174
+ - app/models/coupdoeil/hovercard/option/opening_delay.rb
146
175
  - app/models/coupdoeil/hovercard/option/placement.rb
147
176
  - app/models/coupdoeil/hovercard/option/trigger.rb
148
177
  - app/models/coupdoeil/hovercard/options_set.rb