coupdoeil 1.1.0 → 1.2.0
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 -0
- data/README.md +1 -1
- data/app/assets/javascripts/coupdoeil.js +154 -53
- 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 +27 -14
- data/app/javascript/coupdoeil/popover/attributes.js +6 -2
- data/app/javascript/coupdoeil/popover/closing.js +33 -12
- 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/setup.rb +1 -1
- 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 +6 -8
- 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,17 +1,20 @@
|
|
|
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,
|
|
7
|
+
closeOnHoverNotParentsLater,
|
|
8
8
|
closeTriggeredOnHoverLater,
|
|
9
9
|
closeTriggeredOnHoverNowUnlessAncestor
|
|
10
10
|
} from "../popover/closing";
|
|
11
11
|
import {addToCurrents as addToCurrentPopovers} from "../popover/current";
|
|
12
|
+
import {isAnyPopoverOpened} from "../popover/utils";
|
|
13
|
+
import {upgradeNativeElement} from "../elements/coupdoeil_element";
|
|
12
14
|
|
|
13
15
|
export const onMouseOver = ({ target: hoveredElement }) => {
|
|
14
|
-
const coupdoeilElement = hoveredElement.closest('coup-doeil')
|
|
16
|
+
const coupdoeilElement = hoveredElement.closest('coup-doeil, [data-popover-options]')
|
|
17
|
+
upgradeNativeElement(coupdoeilElement)
|
|
15
18
|
const popoverElement = hoveredElement.closest(POPOVER_SELECTOR)
|
|
16
19
|
|
|
17
20
|
if (coupdoeilElement && popoverElement) {
|
|
@@ -38,6 +41,9 @@ function handleMouseOverCoupdoeilWithinPopover(coupdoeilElement, popoverElement,
|
|
|
38
41
|
// when the mouse goes back from child popover to its coupdoeil element within parent popover
|
|
39
42
|
// it means that this child popover was already open
|
|
40
43
|
closeChildrenNow(childPopover)
|
|
44
|
+
// it should also prevent closing the child popover
|
|
45
|
+
cancelClosingRequest(childPopover)
|
|
46
|
+
addToCurrentPopovers(childPopover.coupdoeilElement)
|
|
41
47
|
} else {
|
|
42
48
|
// ensures to close other children popovers before opening the one that current one
|
|
43
49
|
closeChildrenNow(parentPopover)
|
|
@@ -55,10 +61,10 @@ function handleMouseOverCoupdoeilOutsidePopover(coupdoeilElement, hoveredElement
|
|
|
55
61
|
// Close any other open popover before opening this one
|
|
56
62
|
coupdoeilElement.openPopover(hoveredElement, { beforeDisplay: closeTriggeredOnHoverNowUnlessAncestor })
|
|
57
63
|
} else if (popover.closingRequest) {
|
|
58
|
-
// popover is still open but was requested to close, then it clear this closing request
|
|
59
|
-
// and ensures the popovers stays in current popovers register
|
|
60
|
-
|
|
61
|
-
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)
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -74,16 +80,23 @@ function handleOverOutsideCoupdoeilButWithinPopover(popoverElement) {
|
|
|
74
80
|
const popover = popoverElement.popoverController
|
|
75
81
|
|
|
76
82
|
if (popover.closingRequest) {
|
|
77
|
-
// popover is still open but was requested to close, then it
|
|
78
|
-
// and ensures the popovers stays in current popovers register
|
|
79
|
-
// This typically happens when mouse was on coupdoeil element
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
84
95
|
} else if (popover.children.size > 0) {
|
|
85
96
|
// Happens when a child popover was open but mouse moved outside of it or its coupdoeil element,
|
|
86
97
|
// but stays within the parent popover
|
|
87
98
|
closeOnHoverChildrenLater(popover)
|
|
88
99
|
}
|
|
100
|
+
// Closes all other on hover popovers that are not parents of the current popover nor children.
|
|
101
|
+
closeOnHoverNotParentsLater(popover)
|
|
89
102
|
}
|
|
@@ -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
|
|
|
@@ -92,17 +97,36 @@ export function closeOnHoverChildrenLater(controller) {
|
|
|
92
97
|
})
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
export function closeOnHoverNotParentsLater(controller) {
|
|
101
|
+
let topMostParent = controller
|
|
102
|
+
while (topMostParent.parent) {
|
|
103
|
+
topMostParent = topMostParent.parent
|
|
104
|
+
}
|
|
105
|
+
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
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
|
|
116
|
+
}
|
|
117
|
+
closeLater(coupdoeilElement.popoverController)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
export function closeAllNow() {
|
|
96
122
|
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
97
123
|
closeNow(coupdoeilElement.popoverController)
|
|
98
|
-
removeFromCurrents(coupdoeilElement)
|
|
99
124
|
}
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
export function clearAll() {
|
|
103
128
|
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
104
129
|
clear(coupdoeilElement.popoverController)
|
|
105
|
-
removeFromCurrents(coupdoeilElement)
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
|
|
@@ -110,7 +134,6 @@ export function closeTriggeredOnHoverNow() {
|
|
|
110
134
|
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
111
135
|
if (triggeredOnHover(coupdoeilElement.popoverController)) {
|
|
112
136
|
closeNow(coupdoeilElement.popoverController)
|
|
113
|
-
removeFromCurrents(coupdoeilElement)
|
|
114
137
|
}
|
|
115
138
|
}
|
|
116
139
|
}
|
|
@@ -119,7 +142,6 @@ export function closeTriggeredOnHoverLater() {
|
|
|
119
142
|
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
120
143
|
if (triggeredOnHover(coupdoeilElement.popoverController)) {
|
|
121
144
|
closeLater(coupdoeilElement.popoverController)
|
|
122
|
-
removeFromCurrents(coupdoeilElement)
|
|
123
145
|
}
|
|
124
146
|
}
|
|
125
147
|
}
|
|
@@ -134,7 +156,6 @@ export function closeTriggeredOnHoverNowUnlessAncestor(controller) {
|
|
|
134
156
|
for (const coupdoeilElement of CURRENT_POPOVERS_BY_ID.values()) {
|
|
135
157
|
if (coupdoeilElement.uniqueId !== idToSkip && triggeredOnHover(coupdoeilElement.popoverController)) {
|
|
136
158
|
closeNow(coupdoeilElement.popoverController)
|
|
137
|
-
removeFromCurrents(coupdoeilElement)
|
|
138
159
|
}
|
|
139
160
|
}
|
|
140
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
|
+
}
|