coupdoeil 1.1.1 → 1.2.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.
@@ -4,7 +4,6 @@ module Coupdoeil
4
4
  class PopoversController < ApplicationController
5
5
  before_action :set_action_and_resource_name
6
6
  before_action :set_popover_class
7
- before_action :set_popover_params
8
7
 
9
8
  filters = _process_action_callbacks.map(&:filter) - [:set_action_and_resource_name, :set_popover_class, :set_popover_params]
10
9
  skip_before_action(*filters, raise: false)
@@ -12,14 +11,15 @@ module Coupdoeil
12
11
  skip_around_action(*filters, raise: false)
13
12
 
14
13
  def create
15
- popover = @popover_klass.new(@popover_params, self)
16
- render plain: popover.process(@action_name), layout: false
14
+ popover = @popover_klass.new(params[:params].presence, self)
15
+ popover.lazy_loading! if params[:lazy]
16
+ render html: popover.process(@action_name), layout: false
17
17
  end
18
18
 
19
19
  private
20
20
 
21
21
  def set_action_and_resource_name
22
- @action_name, @resource_name = params[:action_name].split("@")
22
+ @action_name, @resource_name = params.require(:action_name).split("@")
23
23
  end
24
24
 
25
25
  if Rails.env.development?
@@ -31,17 +31,6 @@ module Coupdoeil
31
31
  @popover_klass = Coupdoeil::Popover.registry.lookup(@resource_name)
32
32
  end
33
33
  end
34
-
35
- def set_popover_params
36
- @popover_params =
37
- if params[:params].blank?
38
- @popover_params = {}.with_indifferent_access
39
- else
40
- raw_params = JSON.parse(params[:params])
41
- card_params = Coupdoeil::Params.deserialize(raw_params).sole
42
- card_params.with_indifferent_access
43
- end
44
- end
45
34
  end
46
35
  end
47
36
  ActiveSupport.run_load_hooks(:coupdoeil_popovers_controller, Coupdoeil::PopoversController)
@@ -5,5 +5,18 @@ module Coupdoeil
5
5
  def coupdoeil_popover_tag(popover, popover_options = nil, tag_attributes = nil, &)
6
6
  render(Coupdoeil::Tag.new(popover:, popover_options:, attributes: tag_attributes), &)
7
7
  end
8
+
9
+ def coupdoeil_popover_dataset(popover, popover_options = nil, format: Coupdoeil.config.default_dataset_format)
10
+ coupdoeil_tag = Coupdoeil::Tag.new(popover:, popover_options:, attributes: nil)
11
+ case format
12
+ when :html then tag.attributes(data: coupdoeil_tag.popover_attributes)
13
+ when :nested then { data: coupdoeil_tag.popover_attributes }
14
+ when :prefixed then coupdoeil_tag.popover_attributes(prefixed: true)
15
+ when :raw then coupdoeil_tag.popover_attributes
16
+ else
17
+ raise ArgumentError, "unknown format '#{format}' (#{format.class}). \
18
+ Expected :html, :nested, :prefixed or :raw."
19
+ end
20
+ end
8
21
  end
9
22
  end
@@ -23,7 +23,6 @@ export default class extends HTMLElement {
23
23
  this.openingPopover = true
24
24
 
25
25
  const parent = this.closest(POPOVER_SELECTOR)?.popoverController
26
- addToCurrents(this)
27
26
  return openPopover(this.popoverController, { parent, ...callbacks })
28
27
  }
29
28
 
@@ -43,3 +42,44 @@ export default class extends HTMLElement {
43
42
  }
44
43
  }
45
44
  }
45
+
46
+ export function upgradeNativeElement(element) {
47
+ if (element && element.tagName !== 'COUP-DOEIL' && !element.customCoupdoeilElement) {
48
+ element.customCoupdoeilElement = true
49
+ element.uniqueId = generateUniqueId()
50
+ element.popoverController = new PopoverController(element)
51
+ element.openPopover = function(triggerElement = null, callbacks) {
52
+ if (this.openingPopover || this.popoverController.isOpen || this.disabled) return;
53
+
54
+ this.openingPopover = true
55
+
56
+ const parent = this.closest(POPOVER_SELECTOR)?.popoverController
57
+ addToCurrents(this)
58
+ return openPopover(this.popoverController, { parent, ...callbacks })
59
+ }
60
+ element.closePopover = function() {
61
+ closeNow(this.popoverController)
62
+ }
63
+ }
64
+ }
65
+
66
+ export function downgradeNativeElement(element, {force = false} = {}) {
67
+ if (element.customCoupdoeilElement || force) {
68
+ delete element.closePopover
69
+ delete element.openPopover
70
+ delete element.popoverController
71
+ delete element.uniqueId
72
+ delete element.customCoupdoeilElement
73
+
74
+ delete element.dataset.popoverOptions
75
+ delete element.dataset.popoverParams
76
+ delete element.dataset.popoverType
77
+ delete element.dataset.popoverAnimation
78
+ delete element.dataset.popoverCache
79
+ delete element.dataset.popoverLoading
80
+ delete element.dataset.popoverOffset
81
+ delete element.dataset.popoverOpeningDelay
82
+ delete element.dataset.popoverPlacement
83
+ delete element.dataset.popoverTrigger
84
+ }
85
+ }
@@ -1,10 +1,12 @@
1
1
  import {POPOVER_SELECTOR} from "../popover/config";
2
- import {isElementClosePopoverButton} from "../popover/state_check";
3
2
  import {noTriggeredOnClick} from "../popover/attributes";
4
3
  import {closeAllNow, closeChildrenNow, closeNow} from "../popover/closing";
4
+ import {isElementClosePopoverButton} from "../popover/utils";
5
+ import {upgradeNativeElement} from "../elements/coupdoeil_element";
5
6
 
6
7
  export const coupdoeilOnClickEvent = ({ target: clickedElement }) => {
7
- const coupdoeilElement = clickedElement.closest('coup-doeil')
8
+ const coupdoeilElement = clickedElement.closest('coup-doeil, [data-popover-options]')
9
+ upgradeNativeElement(coupdoeilElement)
8
10
  const popoverElement = clickedElement.closest(POPOVER_SELECTOR)
9
11
 
10
12
  if (coupdoeilElement && popoverElement) {
@@ -1,8 +1,7 @@
1
1
  import {POPOVER_SELECTOR} from "../popover/config";
2
- import {isAnyPopoverOpened} from "../popover/state_check";
3
2
  import {notTriggeredOnHover} from "../popover/attributes";
4
3
  import {
5
- cancelCloseRequest,
4
+ cancelClosingRequest,
6
5
  closeChildrenNow,
7
6
  closeOnHoverChildrenLater,
8
7
  closeOnHoverNotParentsLater,
@@ -10,9 +9,12 @@ import {
10
9
  closeTriggeredOnHoverNowUnlessAncestor
11
10
  } from "../popover/closing";
12
11
  import {addToCurrents as addToCurrentPopovers} from "../popover/current";
12
+ import {isAnyPopoverOpened} from "../popover/utils";
13
+ import {upgradeNativeElement} from "../elements/coupdoeil_element";
13
14
 
14
15
  export const onMouseOver = ({ target: hoveredElement }) => {
15
- const coupdoeilElement = hoveredElement.closest('coup-doeil')
16
+ const coupdoeilElement = hoveredElement.closest('coup-doeil, [data-popover-options]')
17
+ upgradeNativeElement(coupdoeilElement)
16
18
  const popoverElement = hoveredElement.closest(POPOVER_SELECTOR)
17
19
 
18
20
  if (coupdoeilElement && popoverElement) {
@@ -39,6 +41,9 @@ function handleMouseOverCoupdoeilWithinPopover(coupdoeilElement, popoverElement,
39
41
  // when the mouse goes back from child popover to its coupdoeil element within parent popover
40
42
  // it means that this child popover was already open
41
43
  closeChildrenNow(childPopover)
44
+ // it should also prevent closing the child popover
45
+ cancelClosingRequest(childPopover)
46
+ addToCurrentPopovers(childPopover.coupdoeilElement)
42
47
  } else {
43
48
  // ensures to close other children popovers before opening the one that current one
44
49
  closeChildrenNow(parentPopover)
@@ -56,10 +61,10 @@ function handleMouseOverCoupdoeilOutsidePopover(coupdoeilElement, hoveredElement
56
61
  // Close any other open popover before opening this one
57
62
  coupdoeilElement.openPopover(hoveredElement, { beforeDisplay: closeTriggeredOnHoverNowUnlessAncestor })
58
63
  } else if (popover.closingRequest) {
59
- // popover is still open but was requested to close, then it clear this closing request
60
- // and ensures the popovers stays in current popovers register
61
- cancelCloseRequest(popover)
62
- addToCurrentPopovers(coupdoeilElement)
64
+ // If popover is still open but was requested to close, then it must clear this closing request
65
+ // and ensures the popovers stays in current popovers register.
66
+ cancelClosingRequest(popover)
67
+ addToCurrentPopovers(popover.coupdoeilElement)
63
68
  }
64
69
  }
65
70
 
@@ -75,13 +80,18 @@ function handleOverOutsideCoupdoeilButWithinPopover(popoverElement) {
75
80
  const popover = popoverElement.popoverController
76
81
 
77
82
  if (popover.closingRequest) {
78
- // popover is still open but was requested to close, then it clears this closing request
79
- // and ensures the popovers stays in current popovers register
80
- // This typically happens when mouse was on coupdoeil element, then it moves toward the popover
81
- // but because of a small gap, it triggers the closing request, but when the mouse finally enters the popover
82
- // this closing request must be aborted.
83
- cancelCloseRequest(popover)
84
- addToCurrentPopovers(popover.coupdoeilElement)
83
+ // If popover is still open but was requested to close, then it must clear this closing request
84
+ // and ensures the popovers stays in current popovers register.
85
+ // This typically happens when the mouse was on coupdoeil element and moved toward the popover.
86
+ // But because of a small gap, it triggered the closing request. When the mouse finally enters the popover
87
+ // the closing request must be aborted. Since it typically happens in a child popover, it means it should also
88
+ // prevent all parents of this popover to close.
89
+ let topMostParent = popover
90
+ while (topMostParent) {
91
+ cancelClosingRequest(topMostParent)
92
+ addToCurrentPopovers(topMostParent.coupdoeilElement)
93
+ topMostParent = topMostParent.parent
94
+ }
85
95
  } else if (popover.children.size > 0) {
86
96
  // Happens when a child popover was open but mouse moved outside of it or its coupdoeil element,
87
97
  // but stays within the parent popover
@@ -1,11 +1,15 @@
1
1
  import {extractOptionFromElement} from "./options_parser";
2
2
 
3
3
  export function getType(controller) {
4
- return controller.coupdoeilElement.getAttribute('popover-type')
4
+ return controller.coupdoeilElement.dataset.popoverType
5
5
  }
6
6
 
7
7
  export function getParams(controller) {
8
- return controller.coupdoeilElement.getAttribute('popover-params')
8
+ return controller.coupdoeilElement.dataset.popoverParams
9
+ }
10
+
11
+ export function getOptions(controller) {
12
+ return controller.coupdoeilElement.dataset.popoverOptions
9
13
  }
10
14
 
11
15
  export function getTrigger(controller) {
@@ -1,4 +1,4 @@
1
- import {triggeredOnHover} from "./attributes"
1
+ import {triggeredOnHover, triggeredOnClick} from "./attributes"
2
2
  import {CLOSING_DELAY_MS} from "./config"
3
3
  import {leave} from "el-transition"
4
4
  import {addToCurrents, CURRENT_POPOVERS_BY_ID, removeFromCurrents} from "./current"
@@ -12,22 +12,24 @@ function detachFromParent(controller) {
12
12
 
13
13
  export function cancelOpenCloseActions(controller) {
14
14
  cancelOpening(controller)
15
- cancelCloseRequest(controller)
15
+ cancelClosingRequest(controller)
16
16
  }
17
17
 
18
18
  function cancelOpening(controller) {
19
19
  delete controller.coupdoeilElement.openingPopover
20
20
  }
21
21
 
22
- export function cancelCloseRequest(controller) {
23
- clearTimeout(controller.closingRequest)
24
- controller.closingRequest = null
25
- addToCurrents(controller.coupdoeilElement)
22
+ export function cancelClosingRequest(controller) {
23
+ if (controller.closingRequest) {
24
+ clearTimeout(controller.closingRequest)
25
+ controller.closingRequest = null
26
+ }
26
27
  }
27
28
 
28
29
  export function closeNow(controller, allowAnimation = true) {
29
30
  if (controller.closing || (controller.isClosed && !controller.coupdoeilElement.openingPopover)) return
30
31
 
32
+ cancelClosingRequest(controller)
31
33
  controller.closing = true
32
34
 
33
35
  cancelOpenCloseActions(controller)
@@ -39,9 +41,12 @@ export function closeNow(controller, allowAnimation = true) {
39
41
  detachFromParent(controller)
40
42
 
41
43
  if (allowAnimation && controller.card && controller.card.dataset.animation) {
42
- closeWithAnimation(controller)
44
+ closeWithAnimation(controller).then(() => {
45
+ removeFromCurrents(controller.coupdoeilElement)
46
+ })
43
47
  } else {
44
48
  closeWithoutAnimation(controller)
49
+ removeFromCurrents(controller.coupdoeilElement)
45
50
  }
46
51
  }
47
52
 
@@ -98,25 +103,30 @@ export function closeOnHoverNotParentsLater(controller) {
98
103
  topMostParent = topMostParent.parent
99
104
  }
100
105
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
101
- const notTopMostParent = topMostParent.coupdoeilElement.uniqueId !== coupdoeilElement.uniqueId
102
- if (notTopMostParent && triggeredOnHover(coupdoeilElement.popoverController)) {
103
- closeLater(coupdoeilElement.popoverController)
104
- removeFromCurrents(coupdoeilElement)
106
+ if (
107
+ // SKIP If:
108
+ // popover has a parent (Closing topmost parent will close children)
109
+ coupdoeilElement.popoverController.parent !== null ||
110
+ // popover is topmost parent of currently hovered popover (the one whose controller was passed as an argument)
111
+ coupdoeilElement.uniqueId === topMostParent.coupdoeilElement.uniqueId ||
112
+ // popover is triggered on click
113
+ triggeredOnClick(coupdoeilElement.popoverController)
114
+ ) {
115
+ continue
105
116
  }
117
+ closeLater(coupdoeilElement.popoverController)
106
118
  }
107
119
  }
108
120
 
109
121
  export function closeAllNow() {
110
122
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
111
123
  closeNow(coupdoeilElement.popoverController)
112
- removeFromCurrents(coupdoeilElement)
113
124
  }
114
125
  }
115
126
 
116
127
  export function clearAll() {
117
128
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
118
129
  clear(coupdoeilElement.popoverController)
119
- removeFromCurrents(coupdoeilElement)
120
130
  }
121
131
  }
122
132
 
@@ -124,7 +134,6 @@ export function closeTriggeredOnHoverNow() {
124
134
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
125
135
  if (triggeredOnHover(coupdoeilElement.popoverController)) {
126
136
  closeNow(coupdoeilElement.popoverController)
127
- removeFromCurrents(coupdoeilElement)
128
137
  }
129
138
  }
130
139
  }
@@ -133,7 +142,6 @@ export function closeTriggeredOnHoverLater() {
133
142
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
134
143
  if (triggeredOnHover(coupdoeilElement.popoverController)) {
135
144
  closeLater(coupdoeilElement.popoverController)
136
- removeFromCurrents(coupdoeilElement)
137
145
  }
138
146
  }
139
147
  }
@@ -148,7 +156,6 @@ export function closeTriggeredOnHoverNowUnlessAncestor(controller) {
148
156
  for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
149
157
  if (coupdoeilElement.uniqueId !== idToSkip && triggeredOnHover(coupdoeilElement.popoverController)) {
150
158
  closeNow(coupdoeilElement.popoverController)
151
- removeFromCurrents(coupdoeilElement)
152
159
  }
153
160
  }
154
161
  }
@@ -5,7 +5,7 @@ export const POPOVER_CLOSE_BTN_SELECTOR = '[data-popover-close]'
5
5
  // the time (ms) to wait before closing the popover,
6
6
  // to avoid flickering if the user hovers out and in quickly,
7
7
  // or if the user moves the mouse from the target to the popover
8
- export const CLOSING_DELAY_MS = 75
8
+ export const CLOSING_DELAY_MS = 100
9
9
 
10
10
  // the time (ms) to wait before starting to fetch the content,
11
11
  // to avoid fetching too soon if the user hovers in and out the target quickly
@@ -0,0 +1,36 @@
1
+ import {getPopoverContentHTML, setPopoverContentHTML} from "./cache";
2
+ import {positionPopover} from "./positioning";
3
+ import {executeOnNextFrameIfStillOpening, fetchPopoverContent} from "./utils";
4
+
5
+ const MINIMUM_LAZY_DELAY = 600;
6
+
7
+ export function lazyLoadPopoverContent(controller, options) {
8
+ controller.coupdoeilElement.dataset.lazyLoading = "true"
9
+
10
+ // This delay ensures that popover won't blink if the actual content is quickly fetched.
11
+ // It also allows a consistent feeling for users across all lazy loaded popovers.
12
+ const lazyDelay = new Promise(resolve => setTimeout(resolve, MINIMUM_LAZY_DELAY))
13
+
14
+ fetchPopoverContent(controller).then(async (html) => {
15
+ // Wait for the delay if it is not resolved yet.
16
+ await lazyDelay
17
+
18
+ // Update popover content cache with the actual content,
19
+ // but doesn't update yet the rendered popover.
20
+ setPopoverContentHTML(controller, html)
21
+ delete controller.coupdoeilElement.dataset.lazyLoading
22
+
23
+ if (controller.card && !controller.closingRequest) {
24
+ // If popover has not been close are in the process of being closed,
25
+ // update its content with the newly fetch HTML.
26
+ controller.card.innerHTML = getPopoverContentHTML(controller)
27
+
28
+ // Since new content may change the rendered size of the popover,
29
+ // it must be positioned again. It is done one next frame so the browser has at least computed
30
+ // the new dimensions of the popover.
31
+ executeOnNextFrameIfStillOpening(controller, () => {
32
+ positionPopover(controller.coupdoeilElement, controller.card, options)
33
+ })
34
+ }
35
+ })
36
+ }
@@ -1,47 +1,41 @@
1
- import {FETCH_DELAY_MS, POPOVER_CLASS_NAME, OPENING_DELAY_MS} from "./config"
2
- import {getParams, getType, preloadedContentElement, triggeredOnClick} from "./attributes"
1
+ import {FETCH_DELAY_MS, OPENING_DELAY_MS, POPOVER_CLASS_NAME} from "./config"
2
+ import {preloadedContentElement, triggeredOnClick} from "./attributes"
3
3
  import {getPopoverContentHTML, setPopoverContentHTML} from "./cache"
4
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"
8
- import {cancelCloseRequest, clear as clearPopover} from "./closing"
9
-
10
- function fetchPopoverContent(controller) {
11
- const type = getType(controller)
12
- const params = getParams(controller)
13
- const authenticityToken = document.querySelector('meta[name=csrf-token]')?.content
14
- let url = `/coupdoeil/popover`
15
- const opts = {
16
- method: 'POST',
17
- headers: {
18
- 'Content-Type': 'application/json'
19
- },
20
- body: JSON.stringify({ params, action_name: type, authenticity_token: authenticityToken })
21
- }
22
- return fetch(url, opts)
23
- .then((response) => {
24
- if (response.status >= 400) {
25
- throw 'error while fetching popover content'
26
- }
27
- return response.text()
28
- })
29
- }
8
+ import {cancelClosingRequest, clear as clearPopover} from "./closing"
9
+ import {lazyLoadPopoverContent} from "./lazy_loading";
10
+ import {executeOnNextFrameIfStillOpening, fetchPopoverContent} from "./utils";
30
11
 
31
12
  async function loadPopoverContentHTML(controller, options, delayOptions) {
32
13
  return new Promise((resolve) => {
33
14
  setTimeout(async () => {
34
- if (!controller.coupdoeilElement.openingPopover) return // opening has been canceled
35
-
36
- if (options.cache === false || (options.cache && !getPopoverContentHTML(controller))) {
37
- let html
38
- if (options.loading === "preload") {
39
- html = preloadedContentElement(controller).innerHTML
40
- } else {
41
- html = await fetchPopoverContent(controller)
42
- }
43
- setPopoverContentHTML(controller, html)
15
+ // If opening has been canceled then this function aborts. Note that if lazy loading is enable it will still
16
+ // finish to load the HTML to the content cache.
17
+ if (!controller.coupdoeilElement.openingPopover) return resolve()
18
+ // If the cache option is set to true and the content has already been fetched then this function aborts.
19
+ if (getPopoverContentHTML(controller) && options.cache) return resolve()
20
+
21
+ let html
22
+ if (options.loading === "preload") {
23
+ // If loading option is set to 'preload', the preloaded content is present in DOM,
24
+ // nested in the coup-doeil element, in a template tag
25
+ html = preloadedContentElement(controller).innerHTML
26
+ } else if (options.loading === "lazy") {
27
+ // If loading option is set to 'lazy', the HTML is loaded first with the 'lazy' param set to true,
28
+ // to require the temporary/loader content of the popover.
29
+ // At the same time, a second call is triggered with 'lazyLoadPopoverContent' to fetch the actual popover
30
+ // content and update it when received.
31
+ html = await fetchPopoverContent(controller, options.loading === "lazy")
32
+
33
+ lazyLoadPopoverContent(controller, options)
34
+ } else if (options.loading === "async") {
35
+ html = await fetchPopoverContent(controller)
44
36
  }
37
+ // Once the HTML has been retrieved by any of the loading mode, the content cache is updated.
38
+ setPopoverContentHTML(controller, html)
45
39
  resolve()
46
40
  }, delayOptions.fetch)
47
41
  })
@@ -49,7 +43,7 @@ async function loadPopoverContentHTML(controller, options, delayOptions) {
49
43
 
50
44
  export async function openPopover(controller, { parent, beforeDisplay }) {
51
45
  if (controller.isOpen) {
52
- return cancelCloseRequest(controller)
46
+ return cancelClosingRequest(controller)
53
47
  }
54
48
  if (parent) {
55
49
  controller.parent = parent
@@ -74,7 +68,7 @@ export async function openPopover(controller, { parent, beforeDisplay }) {
74
68
  async function display(controller, options, beforeDisplay) {
75
69
  if (controller.isOpen) return;
76
70
 
77
- cancelCloseRequest(controller)
71
+ cancelClosingRequest(controller)
78
72
 
79
73
  if (controller.card) {
80
74
  controller.card.remove()
@@ -86,7 +80,7 @@ async function display(controller, options, beforeDisplay) {
86
80
  controller.card.dataset.animation = options.animation
87
81
  }
88
82
 
89
- executeNextFrameIfStillOpening(controller, async () => {
83
+ executeOnNextFrameIfStillOpening(controller, async () => {
90
84
  await positionPopover(controller.coupdoeilElement, controller.card, options)
91
85
  // popover can be closed while waiting for positioning promise to resolve
92
86
  if (controller.card === null) {
@@ -97,7 +91,7 @@ async function display(controller, options, beforeDisplay) {
97
91
  controller.card.classList.add('hidden')
98
92
  controller.card.style.removeProperty('visibility')
99
93
 
100
- executeNextFrameIfStillOpening(controller, async () => {
94
+ executeOnNextFrameIfStillOpening(controller, async () => {
101
95
  if (beforeDisplay) {
102
96
  beforeDisplay(controller)
103
97
  }
@@ -111,16 +105,6 @@ async function display(controller, options, beforeDisplay) {
111
105
  })
112
106
  }
113
107
 
114
- function executeNextFrameIfStillOpening(controller, callback) {
115
- requestAnimationFrame(() => {
116
- if (controller.card && controller.coupdoeilElement.openingPopover && !controller.closingRequest) {
117
- callback.call()
118
- } else {
119
- clearPopover(controller)
120
- }
121
- })
122
- }
123
-
124
108
  function getDelayOptionsForController(controller, options) {
125
109
  if (options.openingDelay === false || triggeredOnClick(controller)) {
126
110
  return { fetch: 0, opening: 0 }
@@ -1,4 +1,6 @@
1
- const OPTIONS = {
1
+ import {getOptions} from "./attributes";
2
+
3
+ export const OPTIONS = {
2
4
  animation: { getter: getAnimation },
3
5
  cache: { getter: getCache },
4
6
  loading: { getter: getLoading },
@@ -116,7 +118,8 @@ const popoverOptions = {
116
118
  }
117
119
 
118
120
  export function extractOptionsFromElement(coupdoeilElement) {
119
- const optionsInt = coupdoeilElement.popoverController.optionsInt ||= parseOtionsInt((coupdoeilElement))
121
+ const controller = coupdoeilElement.popoverController
122
+ const optionsInt = controller.optionsInt ||= parseOptionsInt((controller))
120
123
  const options = Object.create(popoverOptions)
121
124
 
122
125
  for (const option of ORDERED_OPTIONS) {
@@ -126,13 +129,14 @@ export function extractOptionsFromElement(coupdoeilElement) {
126
129
  return options
127
130
  }
128
131
 
129
- function parseOtionsInt(coupdoeilElement) {
130
- const optionsString = coupdoeilElement.getAttribute("popover-options")
132
+ function parseOptionsInt(controller) {
133
+ const optionsString = getOptions(controller)
131
134
  return parseInt(optionsString, 36)
132
135
  }
133
136
 
134
137
  export function extractOptionFromElement(coupdoeilElement, optionName) {
135
- const optionsInt = coupdoeilElement.popoverController.optionsInt ||= parseOtionsInt((coupdoeilElement))
138
+ const controller = coupdoeilElement.popoverController
139
+ const optionsInt = controller.optionsInt ||= parseOptionsInt((controller))
136
140
 
137
141
  return OPTIONS[optionName].getter(optionsInt)
138
142
  }
@@ -0,0 +1,46 @@
1
+ import {getParams, getType} from "./attributes";
2
+ import {clear as clearPopover} from "./closing";
3
+ import {currentPopoversById} from "./current";
4
+ import {POPOVER_CLOSE_BTN_SELECTOR} from "./config";
5
+
6
+ export function fetchPopoverContent(controller, lazy = false) {
7
+ const type = getType(controller)
8
+ const params = getParams(controller)
9
+ const authenticityToken = document.querySelector('meta[name=csrf-token]')?.content
10
+ let url = `/coupdoeil/popover`
11
+ const opts = {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json'
15
+ },
16
+ body: JSON.stringify({params, action_name: type, authenticity_token: authenticityToken, lazy})
17
+ }
18
+ return fetch(url, opts)
19
+ .then((response) => {
20
+ if (response.status >= 400) {
21
+ throw 'error while fetching popover content'
22
+ }
23
+ return response.text()
24
+ })
25
+ }
26
+
27
+ export function executeOnNextFrameIfStillOpening(controller, callback) {
28
+ requestAnimationFrame(() => {
29
+ const openOrOpening = controller.isOpen || controller.coupdoeilElement.openingPopover;
30
+
31
+ if (controller.card && openOrOpening && !controller.closingRequest) {
32
+ callback.call()
33
+ } else {
34
+ clearPopover(controller)
35
+ }
36
+ })
37
+ }
38
+
39
+ export function isElementClosePopoverButton(element) {
40
+ return element.closest(POPOVER_CLOSE_BTN_SELECTOR) ||
41
+ element.dataset.hasOwnProperty("popoverClose")
42
+ }
43
+
44
+ export function isAnyPopoverOpened() {
45
+ return currentPopoversById().size > 0
46
+ }
@@ -1,6 +1,17 @@
1
1
  import CoupdoeilElement from "./elements/coupdoeil_element"
2
+ import { upgradeNativeElement as internalUpgradeNativeElement, downgradeNativeElement } from "./elements/coupdoeil_element"
2
3
  import './events'
3
4
 
4
5
  if (customElements.get("coup-doeil") === undefined) {
5
6
  customElements.define("coup-doeil", CoupdoeilElement)
6
7
  }
8
+
9
+ window.Coupdoeil = {
10
+ upgradeNativeElement(element) {
11
+ if (!element.dataset.popoverOptions) {
12
+ throw 'element is missing Coupdoeil popover dataset'
13
+ }
14
+ internalUpgradeNativeElement(element)
15
+ },
16
+ downgradeNativeElement
17
+ }
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coupdoeil
4
+ class Popover
5
+ module LazyLoading
6
+ extend ActiveSupport::Concern
7
+
8
+ prepended do
9
+ # Returns true if template is currently rendered for the first phase of lazy loading,
10
+ # which is rendering a simple template as fast as possible before fetch the actual content in second phase.
11
+ # @return [true, false]
12
+ def lazy_loading?
13
+ @lazy_loading
14
+ end
15
+ helper_method :lazy_loading?
16
+
17
+ def loader_variant_for_template?
18
+ loader_template.present?
19
+ end
20
+ helper_method :loader_variant_for_template?
21
+
22
+ def no_loader_variant_for_template?
23
+ loader_template.blank?
24
+ end
25
+ helper_method :no_loader_variant_for_template?
26
+ end
27
+
28
+ # @return [true, false]
29
+ def lazy_loading? = @lazy_loading || false
30
+ def lazy_loading! = @lazy_loading = true
31
+
32
+ # Returns the loader variant for the current action if it exists.
33
+ # @return [ActionView::Template, nil]
34
+ def loader_template
35
+ return @loader_template if defined?(@loader_template)
36
+
37
+ paths = ["#{self.class.popover_resource_name}_popover"]
38
+ found_template = lookup_context.find(action_name, paths, false, [], { variants: :loader })
39
+ @loader_template = found_template.variant == "loader" ? found_template : nil
40
+ end
41
+
42
+ def process(method_name, ...)
43
+ return super unless lazy_loading?
44
+
45
+ self.action_name = method_name
46
+
47
+ instrument_render(method_name) do
48
+ if loader_template
49
+ render template: loader_template
50
+ else
51
+ render html: "", layout: true
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end