1import { Controller } from "stimulus" 2import { setDataActions } from "helpers/stimulus_helpers" 3import { computePosition, shift, flip, offset } from "@floating-ui/dom" 4import { arrow } from "@floating-ui/core" 5 6export default class extends Controller { 7 static targets = ["button", "content", "arrow"] 8 9 static values = { 10 showOnHover: { type: Boolean, default: false }, 11 timeout: { type: Number, default: 200 }, 12 hideTimeout: { type: Number, default: 100 }, 13 placement: { type: String, default: "top" } 14 } 15 16 #contentActions = [ 17 "toggle->tooltip#toggled" 18 ] 19 20 #repositionActions = [ 21 "resize@window->tooltip#reposition", 22 "scroll@window->tooltip#reposition" 23 ] 24 25 #buttonHoverActions = [ 26 "mouseenter->tooltip#showAfterDelay", 27 "mouseleave->tooltip#hideAfterDelay", 28 "focus->tooltip#showAfterDelay", 29 "blur->tooltip#hideAfterDelay", 30 ] 31 32 #contentHoverActions = [ 33 "mouseenter->tooltip#cancelHide", 34 "mouseleave->tooltip#hideAfterDelay" 35 ] 36 37 #timeout 38 #hideTimeout 39 40 connect() { 41 setDataActions(this.contentTarget, this.#contentActions) 42 if (this.showOnHoverValue) { 43 setDataActions(this.buttonTarget, this.#buttonHoverActions) 44 setDataActions(this.contentTarget, this.#contentHoverActions) 45 } 46 } 47 48 disconnect() { 49 clearTimeout(this.#timeout) 50 clearTimeout(this.#hideTimeout) 51 } 52 53 async toggled(event) { 54 if (event.newState === "open") { 55 await this.#computePosition() 56 this.buttonTarget.setAttribute("aria-describedby", this.contentTarget.id) 57 this.contentTarget.classList.add("is-visible") 58 this.#addRepositionListeners() 59 } else { 60 this.buttonTarget.removeAttribute("aria-describedby") 61 this.contentTarget.classList.remove("is-visible") 62 this.#removeRepositionListeners() 63 } 64 } 65 show() { 66 this.contentTarget.showPopover() 67 } 68 69 showAfterDelay() { 70 clearTimeout(this.#hideTimeout) 71 this.#timeout = setTimeout(() => this.show(), this.timeoutValue) 72 } 73 74 hide() { 75 clearTimeout(this.#timeout) 76 clearTimeout(this.#hideTimeout) 77 this.contentTarget.hidePopover() 78 } 79 80 hideAfterDelay(event) { 81 // Don't hide tooltip when user navigates by keyboard - relatedTarget is inside content element. 82 if (this.contentTarget.contains(event.relatedTarget)) return 83 84 clearTimeout(this.#timeout) 85 clearTimeout(this.#hideTimeout) 86 this.#hideTimeout = setTimeout(() => this.hide(), this.hideTimeoutValue) 87 } 88 89 cancelHide() { 90 clearTimeout(this.#hideTimeout) 91 } 92 93 reposition() { 94 this.#computePosition() 95 } 96 97 #addRepositionListeners() { 98 setDataActions(this.element, this.#repositionActions) 99 } 100 101 #removeRepositionListeners() { 102 setDataActions(this.element, this.#repositionActions, { remove: true }) 103 } 104 105 #computePosition() { 106 let middleware = [ 107 offset(12), 108 shift({ mainAxis: true }), 109 flip({ crossAxis: 'alignment', fallbackAxisSideDirection: 'end', padding: 10 }) 110 ] 111 const content = this.contentTarget 112 113 if (this.hasArrowTarget) middleware.push(arrow({ element: this.arrowTarget })) 114 115 return computePosition(this.buttonTarget, content, { 116 placement: this.placementValue, 117 middleware: middleware, 118 }).then(({ x, y, middlewareData, placement }) => { 119 Object.assign(content.style, { 120 left: `${x}px`, 121 top: `${y}px`, 122 }) 123 124 const side = placement.split("-")[0] 125 126 const staticSide = { 127 top: "bottom", 128 right: "left", 129 bottom: "top", 130 left: "right" 131 }[side] 132 133 if (this.hasArrowTarget) { 134 const { x, y } = middlewareData.arrow 135 Object.assign(this.arrowTarget.style, { 136 left: x != null ? `${x}px` : "", 137 top: y != null ? `${y}px` : "", 138 [staticSide]: `${-this.arrowTarget.offsetWidth / 2}px` 139 }) 140 } 141 }) 142 } 143}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.