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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +6 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/config/coupdoeil_manifest.js +1 -0
  7. data/app/assets/javascripts/coupdoeil.js +2289 -0
  8. data/app/assets/javascripts/coupdoeil.min.js +2 -0
  9. data/app/assets/javascripts/coupdoeil.min.js.map +1 -0
  10. data/app/assets/stylesheets/coupdoeil/application.css +15 -0
  11. data/app/assets/stylesheets/coupdoeil/hovercard-animation.css +44 -0
  12. data/app/assets/stylesheets/coupdoeil/hovercard-arrow.css +39 -0
  13. data/app/assets/stylesheets/coupdoeil/hovercard.css +84 -0
  14. data/app/controllers/coupdoeil/hovercards_controller.rb +46 -0
  15. data/app/helpers/coupdoeil/application_helper.rb +16 -0
  16. data/app/javascript/coupdoeil/elements/coupdoeil_element.js +33 -0
  17. data/app/javascript/coupdoeil/events/onclick.js +68 -0
  18. data/app/javascript/coupdoeil/events/onmouseover.js +86 -0
  19. data/app/javascript/coupdoeil/events.js +19 -0
  20. data/app/javascript/coupdoeil/hovercard/actions.js +60 -0
  21. data/app/javascript/coupdoeil/hovercard/attributes.js +33 -0
  22. data/app/javascript/coupdoeil/hovercard/cache.js +18 -0
  23. data/app/javascript/coupdoeil/hovercard/closing.js +81 -0
  24. data/app/javascript/coupdoeil/hovercard/config.js +15 -0
  25. data/app/javascript/coupdoeil/hovercard/controller.js +22 -0
  26. data/app/javascript/coupdoeil/hovercard/opening.js +139 -0
  27. data/app/javascript/coupdoeil/hovercard/optionsParser.js +117 -0
  28. data/app/javascript/coupdoeil/hovercard/positioning.js +74 -0
  29. data/app/javascript/coupdoeil/hovercard/state_check.js +11 -0
  30. data/app/javascript/coupdoeil/hovercard.js +6 -0
  31. data/app/javascript/coupdoeil/index.js +1 -0
  32. data/app/models/coupdoeil/hovercard/option/animation.rb +20 -0
  33. data/app/models/coupdoeil/hovercard/option/cache.rb +19 -0
  34. data/app/models/coupdoeil/hovercard/option/loading.rb +19 -0
  35. data/app/models/coupdoeil/hovercard/option/offset.rb +35 -0
  36. data/app/models/coupdoeil/hovercard/option/placement.rb +44 -0
  37. data/app/models/coupdoeil/hovercard/option/trigger.rb +19 -0
  38. data/app/models/coupdoeil/hovercard/option.rb +45 -0
  39. data/app/models/coupdoeil/hovercard/options_set.rb +57 -0
  40. data/app/models/coupdoeil/hovercard/registry.rb +25 -0
  41. data/app/models/coupdoeil/hovercard/setup.rb +44 -0
  42. data/app/models/coupdoeil/hovercard/view_context_delegation.rb +18 -0
  43. data/app/models/coupdoeil/hovercard.rb +115 -0
  44. data/app/models/coupdoeil/params.rb +83 -0
  45. data/app/models/coupdoeil/tag.rb +45 -0
  46. data/app/style/hovercard-animation.scss +44 -0
  47. data/app/style/hovercard-arrow.scss +40 -0
  48. data/app/style/hovercard.scss +2 -0
  49. data/app/views/layouts/coupdoeil/application.html.erb +15 -0
  50. data/config/routes.rb +3 -0
  51. data/lib/coupdoeil/engine.rb +62 -0
  52. data/lib/coupdoeil/version.rb +3 -0
  53. data/lib/coupdoeil.rb +6 -0
  54. data/lib/generators/coupdoeil/hovercard/USAGE +15 -0
  55. data/lib/generators/coupdoeil/hovercard/hovercard_generator.rb +22 -0
  56. data/lib/generators/coupdoeil/hovercard/templates/hovercard.rb.tt +8 -0
  57. data/lib/generators/coupdoeil/install/install_generator.rb +71 -0
  58. data/lib/generators/coupdoeil/install/templates/layout.html.erb.tt +14 -0
  59. data/lib/tasks/coupdoeil_tasks.rake +4 -0
  60. 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
+ }