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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -2
- data/README.md +2 -2
- data/app/assets/javascripts/coupdoeil.js +143 -57
- data/app/assets/javascripts/coupdoeil.min.js +1 -1
- data/app/assets/javascripts/coupdoeil.min.js.map +1 -1
- data/app/controllers/coupdoeil/popovers_controller.rb +4 -15
- data/app/helpers/coupdoeil/application_helper.rb +13 -0
- data/app/javascript/coupdoeil/elements/coupdoeil_element.js +41 -1
- data/app/javascript/coupdoeil/events/onclick.js +4 -2
- data/app/javascript/coupdoeil/events/onmouseover.js +24 -14
- data/app/javascript/coupdoeil/popover/attributes.js +6 -2
- data/app/javascript/coupdoeil/popover/closing.js +23 -16
- data/app/javascript/coupdoeil/popover/config.js +1 -1
- data/app/javascript/coupdoeil/popover/lazy_loading.js +36 -0
- data/app/javascript/coupdoeil/popover/opening.js +32 -48
- data/app/javascript/coupdoeil/popover/options_parser.js +9 -5
- data/app/javascript/coupdoeil/popover/utils.js +46 -0
- data/app/javascript/coupdoeil/popover.js +11 -0
- data/app/models/coupdoeil/popover/lazy_loading.rb +57 -0
- data/app/models/coupdoeil/popover/option/loading.rb +1 -1
- data/app/models/coupdoeil/popover/option/placement.rb +25 -4
- data/app/models/coupdoeil/popover.rb +27 -12
- data/app/models/coupdoeil/tag.rb +11 -10
- data/lib/coupdoeil/config.rb +7 -0
- data/lib/coupdoeil/engine.rb +2 -1
- data/lib/coupdoeil/version.rb +1 -1
- metadata +4 -6
- data/app/javascript/coupdoeil/popover/actions.js +0 -0
- data/app/javascript/coupdoeil/popover/state_check.js +0 -12
|
@@ -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(
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
-
// and ensures the popovers stays in current popovers register
|
|
80
|
-
// This typically happens when mouse was on coupdoeil element
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
4
|
+
return controller.coupdoeilElement.dataset.popoverType
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function getParams(controller) {
|
|
8
|
-
return controller.coupdoeilElement.
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 =
|
|
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,
|
|
2
|
-
import {
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
130
|
-
const optionsString =
|
|
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
|
|
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
|