coupdoeil 1.0.0.pre.alpha.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 +7 -0
- data/CHANGELOG.md +1 -0
- data/MIT-LICENSE +20 -0
- data/README.md +6 -0
- data/Rakefile +8 -0
- data/app/assets/config/coupdoeil_manifest.js +1 -0
- data/app/assets/javascripts/coupdoeil.js +2289 -0
- data/app/assets/javascripts/coupdoeil.min.js +2 -0
- data/app/assets/javascripts/coupdoeil.min.js.map +1 -0
- data/app/assets/stylesheets/coupdoeil/application.css +15 -0
- data/app/assets/stylesheets/coupdoeil/hovercard-animation.css +44 -0
- data/app/assets/stylesheets/coupdoeil/hovercard-arrow.css +39 -0
- data/app/assets/stylesheets/coupdoeil/hovercard.css +84 -0
- data/app/controllers/coupdoeil/hovercards_controller.rb +46 -0
- data/app/helpers/coupdoeil/application_helper.rb +16 -0
- data/app/javascript/coupdoeil/elements/coupdoeil_element.js +33 -0
- data/app/javascript/coupdoeil/events/onclick.js +68 -0
- data/app/javascript/coupdoeil/events/onmouseover.js +86 -0
- data/app/javascript/coupdoeil/events.js +19 -0
- data/app/javascript/coupdoeil/hovercard/actions.js +60 -0
- data/app/javascript/coupdoeil/hovercard/attributes.js +33 -0
- data/app/javascript/coupdoeil/hovercard/cache.js +18 -0
- data/app/javascript/coupdoeil/hovercard/closing.js +81 -0
- data/app/javascript/coupdoeil/hovercard/config.js +15 -0
- data/app/javascript/coupdoeil/hovercard/controller.js +22 -0
- data/app/javascript/coupdoeil/hovercard/opening.js +139 -0
- data/app/javascript/coupdoeil/hovercard/optionsParser.js +117 -0
- data/app/javascript/coupdoeil/hovercard/positioning.js +74 -0
- data/app/javascript/coupdoeil/hovercard/state_check.js +11 -0
- data/app/javascript/coupdoeil/hovercard.js +6 -0
- data/app/javascript/coupdoeil/index.js +1 -0
- data/app/models/coupdoeil/hovercard/option/animation.rb +20 -0
- data/app/models/coupdoeil/hovercard/option/cache.rb +19 -0
- data/app/models/coupdoeil/hovercard/option/loading.rb +19 -0
- data/app/models/coupdoeil/hovercard/option/offset.rb +35 -0
- data/app/models/coupdoeil/hovercard/option/placement.rb +44 -0
- data/app/models/coupdoeil/hovercard/option/trigger.rb +19 -0
- data/app/models/coupdoeil/hovercard/option.rb +45 -0
- data/app/models/coupdoeil/hovercard/options_set.rb +57 -0
- data/app/models/coupdoeil/hovercard/registry.rb +25 -0
- data/app/models/coupdoeil/hovercard/setup.rb +44 -0
- data/app/models/coupdoeil/hovercard/view_context_delegation.rb +18 -0
- data/app/models/coupdoeil/hovercard.rb +115 -0
- data/app/models/coupdoeil/params.rb +83 -0
- data/app/models/coupdoeil/tag.rb +45 -0
- data/app/style/hovercard-animation.scss +44 -0
- data/app/style/hovercard-arrow.scss +40 -0
- data/app/style/hovercard.scss +2 -0
- data/app/views/layouts/coupdoeil/application.html.erb +15 -0
- data/config/routes.rb +3 -0
- data/lib/coupdoeil/engine.rb +62 -0
- data/lib/coupdoeil/version.rb +3 -0
- data/lib/coupdoeil.rb +6 -0
- data/lib/generators/coupdoeil/hovercard/USAGE +15 -0
- data/lib/generators/coupdoeil/hovercard/hovercard_generator.rb +22 -0
- data/lib/generators/coupdoeil/hovercard/templates/hovercard.rb.tt +8 -0
- data/lib/generators/coupdoeil/install/install_generator.rb +71 -0
- data/lib/generators/coupdoeil/install/templates/layout.html.erb.tt +14 -0
- data/lib/tasks/coupdoeil_tasks.rake +4 -0
- metadata +129 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coupdoeil
|
4
|
+
class HovercardsController < ApplicationController
|
5
|
+
before_action :set_action_and_resource_name
|
6
|
+
before_action :set_hovercard_class
|
7
|
+
before_action :set_hovercard_params
|
8
|
+
|
9
|
+
filters = _process_action_callbacks.map(&:filter) - %i[set_action_and_resource_name set_hovercard_class set_hovercard_params]
|
10
|
+
skip_before_action(*filters, raise: false)
|
11
|
+
skip_after_action(*filters, raise: false)
|
12
|
+
skip_around_action(*filters, raise: false)
|
13
|
+
|
14
|
+
def create
|
15
|
+
hovercard = @hovercard_klass.new(@hovercard_params, view_context)
|
16
|
+
render plain: hovercard.process(@action_name), layout: false
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def set_action_and_resource_name
|
22
|
+
@action_name, @resource_name = params[:action_name].split("@")
|
23
|
+
end
|
24
|
+
|
25
|
+
if Rails.env.development?
|
26
|
+
def set_hovercard_class
|
27
|
+
@hovercard_klass = Coupdoeil::Hovercard.registry.lookup_or_register(@resource_name)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
def set_hovercard_class
|
31
|
+
@hovercard_klass = Coupdoeil::Hovercard.registry.lookup(@resource_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_hovercard_params
|
36
|
+
@hovercard_params =
|
37
|
+
if params[:params].blank?
|
38
|
+
@hovercard_params = {}.freeze
|
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
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Coupdoeil
|
4
|
+
module ApplicationHelper
|
5
|
+
def coupdoeil_hovercard_tag(hovercard, options = nil, **attributes_or_options, &)
|
6
|
+
if options.present?
|
7
|
+
attributes = attributes_or_options
|
8
|
+
else
|
9
|
+
options = attributes_or_options.extract!(*Hovercard::OptionsSet::OPTION_NAMES)
|
10
|
+
attributes = attributes_or_options
|
11
|
+
end
|
12
|
+
hovercard_options = options
|
13
|
+
render(Coupdoeil::Tag.new(hovercard:, hovercard_options:, attributes:), &)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import {HovercardController} from '../hovercard/controller'
|
2
|
+
import {openHovercard} from '../hovercard/opening'
|
3
|
+
import {addToCurrents} from "../hovercard/actions";
|
4
|
+
import {HOVERCARD_SELECTOR} from "../hovercard/config";
|
5
|
+
import {closeNow} from "../hovercard/closing";
|
6
|
+
|
7
|
+
function generateUniqueId() {
|
8
|
+
const array = new Uint32Array(1)
|
9
|
+
window.crypto.getRandomValues(array)
|
10
|
+
return array[0]
|
11
|
+
}
|
12
|
+
|
13
|
+
export default class extends HTMLElement {
|
14
|
+
constructor() {
|
15
|
+
super()
|
16
|
+
this.uniqueId = generateUniqueId()
|
17
|
+
this.hovercardController = new HovercardController(this)
|
18
|
+
}
|
19
|
+
|
20
|
+
openHovercard() {
|
21
|
+
if (this.openingHovercard || this.hovercardController.isOpen) return;
|
22
|
+
|
23
|
+
this.openingHovercard = true
|
24
|
+
|
25
|
+
const parent = this.closest(HOVERCARD_SELECTOR)?.controller
|
26
|
+
addToCurrents(this)
|
27
|
+
return openHovercard(this.hovercardController, { parent })
|
28
|
+
}
|
29
|
+
|
30
|
+
closeHovercard() {
|
31
|
+
closeNow(this.hovercardController)
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import {HOVERCARD_SELECTOR} from "../hovercard/config";
|
2
|
+
import {isElementCloseHovercardButton} from "../hovercard/state_check";
|
3
|
+
import {closeAllNow} from "../hovercard/actions";
|
4
|
+
import {noTriggeredOnClick} from "../hovercard/attributes";
|
5
|
+
import {closeChildrenNow, closeNow} from "../hovercard/closing";
|
6
|
+
|
7
|
+
export const coupdoeilOnClickEvent = ({ target: clickedElement }) => {
|
8
|
+
const coupdoeilElement = clickedElement.closest('coup-doeil')
|
9
|
+
const hovercardElement = clickedElement.closest(HOVERCARD_SELECTOR)
|
10
|
+
|
11
|
+
if (coupdoeilElement && hovercardElement) {
|
12
|
+
handleClickedCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement)
|
13
|
+
}
|
14
|
+
else if (coupdoeilElement) {
|
15
|
+
handleClickedCoupdoeilOutsideHovercard(coupdoeilElement)
|
16
|
+
}
|
17
|
+
else if (hovercardElement) {
|
18
|
+
handleClickOutsideCoupdoeilButWithinHovercard(hovercardElement, clickedElement)
|
19
|
+
}
|
20
|
+
else {
|
21
|
+
handleClickOutsideCoupdoeilAndHovercard()
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
function handleClickedCoupdoeilWithinHovercard(coupdoeilElement, _hovercardElement) {
|
26
|
+
const hovercard = coupdoeilElement.hovercardController
|
27
|
+
if(noTriggeredOnClick(hovercard))
|
28
|
+
return;
|
29
|
+
|
30
|
+
if (hovercard.isOpen) {
|
31
|
+
// second click on an open hovercard trigger closes it
|
32
|
+
closeNow(hovercard)
|
33
|
+
} else {
|
34
|
+
// first click on a closed hovercard trigger opens it
|
35
|
+
// If any other hovercard is open, it is the parent hovercard, hence it should not be closed.
|
36
|
+
coupdoeilElement.openHovercard()
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
function handleClickedCoupdoeilOutsideHovercard(coupdoeilElement) {
|
41
|
+
const hovercard = coupdoeilElement.hovercardController
|
42
|
+
if(noTriggeredOnClick(hovercard))
|
43
|
+
return;
|
44
|
+
|
45
|
+
if (hovercard.isOpen) {
|
46
|
+
// second click on an open hovercard trigger closes it
|
47
|
+
closeNow(hovercard)
|
48
|
+
} else {
|
49
|
+
// close any other open hovercard
|
50
|
+
closeAllNow()
|
51
|
+
// first click on a closed hovercard trigger opens it
|
52
|
+
coupdoeilElement.openHovercard()
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
function handleClickOutsideCoupdoeilButWithinHovercard(hovercardElement, clickedElement) {
|
57
|
+
const hovercard = hovercardElement.controller;
|
58
|
+
|
59
|
+
if (isElementCloseHovercardButton(clickedElement)) {
|
60
|
+
closeNow(hovercard)
|
61
|
+
} else if (hovercard.children.size > 0) {
|
62
|
+
closeChildrenNow(hovercard)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
function handleClickOutsideCoupdoeilAndHovercard() {
|
67
|
+
closeAllNow()
|
68
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import {HOVERCARD_SELECTOR} from "../hovercard/config";
|
2
|
+
import {isAnyHovercardOpened} from "../hovercard/state_check";
|
3
|
+
import {
|
4
|
+
addToCurrents as addToCurrentHovercards,
|
5
|
+
closeTriggeredOnHoverLater, closeTriggeredOnHoverNow
|
6
|
+
} from "../hovercard/actions";
|
7
|
+
import {notTriggeredOnHover} from "../hovercard/attributes";
|
8
|
+
import {cancelCloseRequest, closeChildrenNow, closeOnHoverChildrenLater} from "../hovercard/closing";
|
9
|
+
|
10
|
+
export const onMouseOver = ({ target: hoveredElement }) => {
|
11
|
+
const coupdoeilElement = hoveredElement.closest('coup-doeil')
|
12
|
+
const hovercardElement = hoveredElement.closest(HOVERCARD_SELECTOR)
|
13
|
+
|
14
|
+
if (coupdoeilElement && hovercardElement) {
|
15
|
+
handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement)
|
16
|
+
}
|
17
|
+
else if (coupdoeilElement) {
|
18
|
+
handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement)
|
19
|
+
}
|
20
|
+
else if (hovercardElement) {
|
21
|
+
handleOverOutsideCoupdoeilButWithinHovercard(hovercardElement)
|
22
|
+
}
|
23
|
+
else {
|
24
|
+
handleOverOutsideCoupdoeilAndHovercard()
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
function handleMouseOverCoupdoeilWithinHovercard(coupdoeilElement, hovercardElement) {
|
29
|
+
const childHovercard = coupdoeilElement.hovercardController
|
30
|
+
const parentHovercard = hovercardElement.controller
|
31
|
+
if(notTriggeredOnHover(childHovercard))
|
32
|
+
return;
|
33
|
+
|
34
|
+
if (childHovercard.isOpen) {
|
35
|
+
// when mouse goes back from child hovercard to its coupdoeil element within parent hovercard
|
36
|
+
// it means that this child hovercard was already open
|
37
|
+
closeChildrenNow(childHovercard)
|
38
|
+
} else {
|
39
|
+
// ensures to close other children hovercards before opening the one that current one
|
40
|
+
closeChildrenNow(parentHovercard)
|
41
|
+
coupdoeilElement.openHovercard()
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
function handleMouseOverCoupdoeilOutsideHovercard(coupdoeilElement) {
|
46
|
+
const hovercard = coupdoeilElement.hovercardController
|
47
|
+
if(notTriggeredOnHover(hovercard))
|
48
|
+
return;
|
49
|
+
|
50
|
+
if (hovercard.isClosed) {
|
51
|
+
// Close any other open hovercard before opening this one
|
52
|
+
closeTriggeredOnHoverNow()
|
53
|
+
coupdoeilElement.openHovercard()
|
54
|
+
} else if (hovercard.closingRequest) {
|
55
|
+
// hovercard is still open but was requested to close, then it clear this closing request
|
56
|
+
// and ensures the hovercards stays in current hovercards register
|
57
|
+
cancelCloseRequest(hovercard)
|
58
|
+
addToCurrentHovercards(coupdoeilElement)
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
function handleOverOutsideCoupdoeilAndHovercard() {
|
63
|
+
// mouse is not within any hovercard and not over any coupdoeil element
|
64
|
+
// Therefore all hovercards that trigger on hover should be closed if any is open.
|
65
|
+
if (isAnyHovercardOpened()) {
|
66
|
+
closeTriggeredOnHoverLater()
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
function handleOverOutsideCoupdoeilButWithinHovercard(hovercardElement) {
|
71
|
+
const hovercard = hovercardElement.controller
|
72
|
+
|
73
|
+
if (hovercard.closingRequest) {
|
74
|
+
// hovercard is still open but was requested to close, then it clears this closing request
|
75
|
+
// and ensures the hovercards stays in current hovercards register
|
76
|
+
// This typically happens when mouse was on coupdoeil element, then it moves toward the hovercard
|
77
|
+
// but because of a small gap, it triggers the closing request, but when the mouse finally enters the hovercard
|
78
|
+
// this closing request must be aborted.
|
79
|
+
cancelCloseRequest(hovercard)
|
80
|
+
addToCurrentHovercards(hovercard.coupdoeilElement)
|
81
|
+
} else if (hovercard.children.size > 0) {
|
82
|
+
// Happens when a child hovercard was open but mouse moved outside of it or its coupdoeil element,
|
83
|
+
// but stays within the parent hovercard
|
84
|
+
closeOnHoverChildrenLater(hovercard)
|
85
|
+
}
|
86
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import {coupdoeilOnClickEvent} from "./events/onclick";
|
2
|
+
import {onMouseOver} from "./events/onmouseover";
|
3
|
+
import {clearHovercardContentCache, clearAll} from "./hovercard/actions";
|
4
|
+
|
5
|
+
document.addEventListener("DOMContentLoaded", () => {
|
6
|
+
clearHovercardContentCache()
|
7
|
+
document.addEventListener("click", coupdoeilOnClickEvent)
|
8
|
+
document.documentElement.addEventListener("mouseover", onMouseOver, { passive: true })
|
9
|
+
|
10
|
+
if (window.Turbo) {
|
11
|
+
document.addEventListener('turbo:before-cache', (_event) => {
|
12
|
+
clearAll()
|
13
|
+
})
|
14
|
+
document.addEventListener('turbo:load', (_event) => {
|
15
|
+
clearAll()
|
16
|
+
clearHovercardContentCache()
|
17
|
+
})
|
18
|
+
}
|
19
|
+
})
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import {hovercardContentHTMLMap} from "./cache";
|
2
|
+
import {triggeredOnHover} from "./attributes";
|
3
|
+
import {clear as clearHovercard, closeLater, closeNow, cancelCloseRequest} from "./closing";
|
4
|
+
import {cancelOpening} from "./opening";
|
5
|
+
|
6
|
+
const CURRENT_HOVERCARDS_BY_ID = new Map()
|
7
|
+
window.hovercads = CURRENT_HOVERCARDS_BY_ID
|
8
|
+
|
9
|
+
export function clearHovercardContentCache() {
|
10
|
+
hovercardContentHTMLMap.clear()
|
11
|
+
}
|
12
|
+
|
13
|
+
export function currentHovercardsById() {
|
14
|
+
return CURRENT_HOVERCARDS_BY_ID
|
15
|
+
}
|
16
|
+
|
17
|
+
export function addToCurrents(coupdoeilElement) {
|
18
|
+
CURRENT_HOVERCARDS_BY_ID.set(coupdoeilElement.uniqueId, coupdoeilElement)
|
19
|
+
}
|
20
|
+
|
21
|
+
export function closeAllNow() {
|
22
|
+
for (const coupdoeilElement of CURRENT_HOVERCARDS_BY_ID.values()) {
|
23
|
+
closeNow(coupdoeilElement.hovercardController)
|
24
|
+
removeFromCurrents(coupdoeilElement)
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
export function clearAll() {
|
29
|
+
for (const coupdoeilElement of CURRENT_HOVERCARDS_BY_ID.values()) {
|
30
|
+
clearHovercard(coupdoeilElement.hovercardController)
|
31
|
+
removeFromCurrents(coupdoeilElement)
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
export function closeTriggeredOnHoverNow() {
|
36
|
+
for (const coupdoeilElement of CURRENT_HOVERCARDS_BY_ID.values()) {
|
37
|
+
if (triggeredOnHover(coupdoeilElement.hovercardController)) {
|
38
|
+
closeNow(coupdoeilElement.hovercardController)
|
39
|
+
removeFromCurrents(coupdoeilElement)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
export function closeTriggeredOnHoverLater() {
|
45
|
+
for (const coupdoeilElement of CURRENT_HOVERCARDS_BY_ID.values()) {
|
46
|
+
if (triggeredOnHover(coupdoeilElement.hovercardController)) {
|
47
|
+
closeLater(coupdoeilElement.hovercardController)
|
48
|
+
removeFromCurrents(coupdoeilElement)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
export function removeFromCurrents(coupdoeilElement) {
|
54
|
+
CURRENT_HOVERCARDS_BY_ID.delete(coupdoeilElement.uniqueId)
|
55
|
+
}
|
56
|
+
|
57
|
+
export function cancelOpenCloseActions(controller) {
|
58
|
+
cancelOpening(controller)
|
59
|
+
cancelCloseRequest(controller)
|
60
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import {extractOptionFromElement} from "./optionsParser";
|
2
|
+
|
3
|
+
export function getType(controller) {
|
4
|
+
return controller.coupdoeilElement.getAttribute('hc-type')
|
5
|
+
}
|
6
|
+
|
7
|
+
export function getParams(controller) {
|
8
|
+
return controller.coupdoeilElement.getAttribute('hc-params')
|
9
|
+
}
|
10
|
+
|
11
|
+
export function getTrigger(controller) {
|
12
|
+
return extractOptionFromElement(controller.coupdoeilElement, 'trigger')
|
13
|
+
}
|
14
|
+
|
15
|
+
export function triggeredOnClick(controller) {
|
16
|
+
return getTrigger(controller) === 'click'
|
17
|
+
}
|
18
|
+
|
19
|
+
export function noTriggeredOnClick(controller) {
|
20
|
+
return getTrigger(controller) !== 'click'
|
21
|
+
}
|
22
|
+
|
23
|
+
export function triggeredOnHover(controller) {
|
24
|
+
return getTrigger(controller) === 'hover'
|
25
|
+
}
|
26
|
+
|
27
|
+
export function notTriggeredOnHover(controller) {
|
28
|
+
return getTrigger(controller) !== 'hover'
|
29
|
+
}
|
30
|
+
|
31
|
+
export function preloadedContentElement(controller) {
|
32
|
+
return controller.coupdoeilElement.querySelector('.hovercard-content')
|
33
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import {getType, getParams, preloadedContentElement} from './attributes'
|
2
|
+
|
3
|
+
export const hovercardContentHTMLMap = new Map()
|
4
|
+
|
5
|
+
function cacheMapKey(controller) {
|
6
|
+
if (preloadedContentElement(controller)) {
|
7
|
+
return controller.coupdoeilElement.uniqueId
|
8
|
+
}
|
9
|
+
return getType(controller) + getParams(controller)
|
10
|
+
}
|
11
|
+
|
12
|
+
export function getHovercardContentHTML(controller) {
|
13
|
+
return hovercardContentHTMLMap.get(cacheMapKey(controller))
|
14
|
+
}
|
15
|
+
|
16
|
+
export function setHovercardContentHTML(controller, value) {
|
17
|
+
hovercardContentHTMLMap.set(cacheMapKey(controller), value)
|
18
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import {triggeredOnHover} from "./attributes";
|
2
|
+
import {defaultConfig} from "./config";
|
3
|
+
import {leave} from "el-transition";
|
4
|
+
import {cancelOpenCloseActions} from "./actions";
|
5
|
+
|
6
|
+
function detachFromParent(controller) {
|
7
|
+
if (controller.parent) {
|
8
|
+
controller.parent.children.delete(controller)
|
9
|
+
controller.parent = null
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
export function cancelCloseRequest(controller) {
|
14
|
+
clearTimeout(controller.closingRequest)
|
15
|
+
controller.closingRequest = null
|
16
|
+
}
|
17
|
+
|
18
|
+
export function closeNow(controller, allowAnimation = true) {
|
19
|
+
if (controller.closing || controller.isClosed) return;
|
20
|
+
|
21
|
+
controller.closing = true
|
22
|
+
|
23
|
+
cancelOpenCloseActions(controller)
|
24
|
+
|
25
|
+
controller.children.forEach((childController) => {
|
26
|
+
closeNow(childController)
|
27
|
+
})
|
28
|
+
|
29
|
+
detachFromParent(controller)
|
30
|
+
|
31
|
+
if (allowAnimation && controller.card.dataset.animation) {
|
32
|
+
closeWithAnimation(controller)
|
33
|
+
} else {
|
34
|
+
closeWithoutAnimation(controller)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
async function closeWithAnimation(controller) {
|
39
|
+
await leave(controller.card, 'hovercard')
|
40
|
+
|
41
|
+
closeWithoutAnimation(controller)
|
42
|
+
}
|
43
|
+
|
44
|
+
function closeWithoutAnimation(controller) {
|
45
|
+
controller.card.remove()
|
46
|
+
controller.card = null
|
47
|
+
delete controller.closing
|
48
|
+
delete controller.coupdoeilElement.dataset.hovercardOpen
|
49
|
+
}
|
50
|
+
|
51
|
+
export function clear(controller) {
|
52
|
+
closeNow(controller, false)
|
53
|
+
}
|
54
|
+
|
55
|
+
export function closeLater(controller) {
|
56
|
+
cancelOpenCloseActions(controller)
|
57
|
+
controller.closingRequest = setTimeout(() => {
|
58
|
+
closeNow(controller)
|
59
|
+
}, defaultConfig.closingDelay)
|
60
|
+
}
|
61
|
+
|
62
|
+
export function closeChildrenNow(controller) {
|
63
|
+
controller.children.forEach((childController) => {
|
64
|
+
closeNow(childController)
|
65
|
+
})
|
66
|
+
}
|
67
|
+
|
68
|
+
export function closeChildrenLater(controller) {
|
69
|
+
controller.children.forEach((childController) => {
|
70
|
+
closeLater(childController)
|
71
|
+
})
|
72
|
+
}
|
73
|
+
|
74
|
+
|
75
|
+
export function closeOnHoverChildrenLater(controller) {
|
76
|
+
controller.children.forEach((childController) => {
|
77
|
+
if (triggeredOnHover(childController)) {
|
78
|
+
closeLater(childController)
|
79
|
+
}
|
80
|
+
})
|
81
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export const HOVERCARD_CLASS_NAME = 'coupdoeil--hovercard'
|
2
|
+
export const HOVERCARD_SELECTOR = `.${HOVERCARD_CLASS_NAME}`
|
3
|
+
export const HOVERCARD_CLOSE_BTN_SELECTOR = '[data-hovercard-close]'
|
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
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
export class HovercardController {
|
2
|
+
constructor(coupdoeilElement) {
|
3
|
+
this.coupdoeilElement = coupdoeilElement
|
4
|
+
|
5
|
+
this.card = null // can go on coupdoeil element, renamed 'hovercardElement'
|
6
|
+
|
7
|
+
this.children = new Set() // can go on hovercardElement
|
8
|
+
this.parent = null // can go on hovercardElement
|
9
|
+
|
10
|
+
this.closingRequest = null // can go on coupdoeil element
|
11
|
+
this.openingDelay = null // can go on coupdoeil element
|
12
|
+
this.fetchDelay = null // can go on coupdoeil element
|
13
|
+
}
|
14
|
+
|
15
|
+
get isOpen() {
|
16
|
+
return !!this.coupdoeilElement.dataset.hovercardOpen
|
17
|
+
}
|
18
|
+
|
19
|
+
get isClosed() {
|
20
|
+
return !this.isOpen
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import {defaultConfig, HOVERCARD_CLASS_NAME} from "./config";
|
2
|
+
import {getParams, getType, preloadedContentElement, triggeredOnClick} from "./attributes";
|
3
|
+
import {getHovercardContentHTML, setHovercardContentHTML} from "./cache";
|
4
|
+
import {addToCurrents} from "./actions";
|
5
|
+
import {extractOptionsFromElement} from "./optionsParser";
|
6
|
+
import {cancelCloseRequest} from "./closing";
|
7
|
+
import {positionHovercard} from "./positioning";
|
8
|
+
import {enter} from "el-transition";
|
9
|
+
|
10
|
+
export function cancelOpening(controller) {
|
11
|
+
clearTimeout(controller.openingDelay)
|
12
|
+
controller.openingDelay = null
|
13
|
+
clearTimeout(controller.fetchDelay)
|
14
|
+
controller.fetchDelay = null
|
15
|
+
delete controller.coupdoeilElement.openingHovercard
|
16
|
+
}
|
17
|
+
|
18
|
+
export async function openHovercard(controller, { parent }) {
|
19
|
+
if (controller.isOpen) {
|
20
|
+
cancelCloseRequest(controller)
|
21
|
+
return addToCurrents(controller.coupdoeilElement)
|
22
|
+
}
|
23
|
+
if (parent) {
|
24
|
+
controller.parent = parent
|
25
|
+
parent.children.add(controller)
|
26
|
+
}
|
27
|
+
|
28
|
+
const delayOptions = getDelayOptionsForController(controller)
|
29
|
+
const options = extractOptionsFromElement(controller.coupdoeilElement)
|
30
|
+
const { cache } = options
|
31
|
+
|
32
|
+
controller.openingDelay = new Promise((resolve) => {
|
33
|
+
if (getHovercardContentHTML(controller) && cache) {
|
34
|
+
setTimeout(resolve, delayOptions.reOpening)
|
35
|
+
} else {
|
36
|
+
setTimeout(resolve, delayOptions.actualOpening)
|
37
|
+
}
|
38
|
+
})
|
39
|
+
if (!getHovercardContentHTML(controller) || !cache) {
|
40
|
+
// prevent fetching if the user hovers in and out quickly
|
41
|
+
controller.fetchDelay = new Promise((resolve) => {
|
42
|
+
setTimeout(resolve, delayOptions.fetch)
|
43
|
+
})
|
44
|
+
await controller.fetchDelay
|
45
|
+
if (!controller.fetchDelay) {
|
46
|
+
return
|
47
|
+
}
|
48
|
+
controller.fetchDelay = null
|
49
|
+
const html = preloadedContentElement(controller)?.innerHTML || await fetchHovercardContent(controller)
|
50
|
+
setHovercardContentHTML(controller, html)
|
51
|
+
}
|
52
|
+
// still await the delay even if content is already fetched
|
53
|
+
await controller.openingDelay
|
54
|
+
const parentIsClosedOrClosing = controller.parent && (controller.parent.isClosed || controller.parent.closingRequest)
|
55
|
+
// but if opening has been cancelled (nullified), the wait still happens, so we need to check again
|
56
|
+
if (controller.openingDelay && !parentIsClosedOrClosing) {
|
57
|
+
controller.openingDelay = null
|
58
|
+
await display(controller, options)
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
async function display(controller, options) {
|
63
|
+
if (controller.isOpen) return;
|
64
|
+
|
65
|
+
cancelCloseRequest(controller)
|
66
|
+
|
67
|
+
controller.card = buildHovercardElement(controller, options)
|
68
|
+
document.body.appendChild(controller.card)
|
69
|
+
|
70
|
+
if (options.animation) {
|
71
|
+
controller.card.dataset.animation = options.animation
|
72
|
+
}
|
73
|
+
|
74
|
+
requestAnimationFrame(async () => {
|
75
|
+
controller.card.style.opacity = '0'
|
76
|
+
controller.card.classList.remove('hidden')
|
77
|
+
|
78
|
+
requestAnimationFrame(async () => {
|
79
|
+
await positionHovercard(controller, options)
|
80
|
+
|
81
|
+
controller.card.classList.add('hidden')
|
82
|
+
controller.card.style.removeProperty('opacity')
|
83
|
+
|
84
|
+
requestAnimationFrame(async () => {
|
85
|
+
// adding again the card to make sure it is in the map, could be better
|
86
|
+
addToCurrents(controller.coupdoeilElement)
|
87
|
+
delete controller.coupdoeilElement.openingHovercard
|
88
|
+
controller.coupdoeilElement.dataset.hovercardOpen = true
|
89
|
+
|
90
|
+
await enter(controller.card, 'hovercard')
|
91
|
+
})
|
92
|
+
})
|
93
|
+
})
|
94
|
+
}
|
95
|
+
|
96
|
+
function getDelayOptionsForController(controller) {
|
97
|
+
if (triggeredOnClick(controller)) {
|
98
|
+
return { reOpening: 0, actualOpening: 0, fetch: 0 }
|
99
|
+
}
|
100
|
+
return {
|
101
|
+
fetch: defaultConfig.fetchDelay,
|
102
|
+
// the time (ms) to wait if we already have fetched the content
|
103
|
+
reOpening: defaultConfig.fetchDelay + defaultConfig.openingDelay,
|
104
|
+
// the time (ms) to wait if we already have waited to fetch the content
|
105
|
+
actualOpening: defaultConfig.openingDelay - defaultConfig.fetchDelay
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
function fetchHovercardContent(controller) {
|
110
|
+
const type = getType(controller)
|
111
|
+
const params = getParams(controller)
|
112
|
+
const authenticityToken = document.querySelector('meta[name=csrf-token]').content
|
113
|
+
let url = `/coupdoeil/hovercard`
|
114
|
+
const opts = {
|
115
|
+
method: 'POST',
|
116
|
+
headers: {
|
117
|
+
'Content-Type': 'application/json'
|
118
|
+
},
|
119
|
+
body: JSON.stringify({ params, action_name: type, authenticity_token: authenticityToken })
|
120
|
+
}
|
121
|
+
return fetch(url, opts)
|
122
|
+
.then((response) => {
|
123
|
+
if (response.status >= 400) {
|
124
|
+
throw 'error while fetching hovercard content'
|
125
|
+
}
|
126
|
+
return response.text()
|
127
|
+
})
|
128
|
+
}
|
129
|
+
|
130
|
+
function buildHovercardElement(controller, options) {
|
131
|
+
const el = document.createElement('div')
|
132
|
+
el.setAttribute('role', 'dialog')
|
133
|
+
el.classList.add(HOVERCARD_CLASS_NAME, 'hidden')
|
134
|
+
el.style.cssText = 'position: absolute; left: 0; top: 0;'
|
135
|
+
el.innerHTML = getHovercardContentHTML(controller)
|
136
|
+
el.controller = controller
|
137
|
+
el.dataset.placement = options.placement
|
138
|
+
return el
|
139
|
+
}
|