coupdoeil
Version:
Javascript for Ruby on Rails Coupdoeil gem
134 lines (112 loc) • 5.51 kB
JavaScript
import {FETCH_DELAY_MS, OPENING_DELAY_MS, POPOVER_CLASS_NAME} from "./config"
import {preloadedContentElement, triggeredOnClick} from "./attributes"
import {getPopoverContentHTML, setPopoverContentHTML} from "./cache"
import {extractOptionsFromElement} from "./options_parser"
import {positionPopover} from "./positioning"
import {enter} from "el-transition"
import {addToCurrents} from "./current"
import {cancelClosingRequest, clear as clearPopover} from "./closing"
import {lazyLoadPopoverContent} from "./lazy_loading";
import {executeOnNextFrameIfStillOpening, fetchPopoverContent} from "./utils";
async function loadPopoverContentHTML(controller, options, delayOptions) {
return new Promise((resolve) => {
setTimeout(async () => {
// If opening has been canceled then this function aborts. Note that if lazy loading is enable it will still
// finish to load the HTML to the content cache.
if (!controller.coupdoeilElement.openingPopover) return resolve()
// If the cache option is set to true and the content has already been fetched then this function aborts.
if (getPopoverContentHTML(controller) && options.cache) return resolve()
let html
if (options.loading === "preload") {
// If loading option is set to 'preload', the preloaded content is present in DOM,
// nested in the coup-doeil element, in a template tag
html = preloadedContentElement(controller).innerHTML
} else if (options.loading === "lazy") {
// If loading option is set to 'lazy', the HTML is loaded first with the 'lazy' param set to true,
// to require the temporary/loader content of the popover.
// At the same time, a second call is triggered with 'lazyLoadPopoverContent' to fetch the actual popover
// content and update it when received.
html = await fetchPopoverContent(controller, options.loading === "lazy")
lazyLoadPopoverContent(controller, options)
} else if (options.loading === "async") {
html = await fetchPopoverContent(controller)
}
// Once the HTML has been retrieved by any of the loading mode, the content cache is updated.
setPopoverContentHTML(controller, html)
resolve()
}, delayOptions.fetch)
})
}
export async function openPopover(controller, { parent, beforeDisplay }) {
if (controller.isOpen) {
return cancelClosingRequest(controller)
}
if (parent) {
controller.parent = parent
parent.children.add(controller)
}
const options = extractOptionsFromElement(controller.coupdoeilElement)
const delays = getDelayOptionsForController(controller, options)
const openingDelay = new Promise(resolve => setTimeout(resolve, delays.opening))
const fetchDelay = loadPopoverContentHTML(controller, options, delays)
await Promise.all([fetchDelay, openingDelay])
const parentIsClosedOrClosing = controller.parent && (controller.parent.isClosed || controller.parent.closingRequest)
// but if opening has been canceled (nullified), the wait still happens, so we need to check again
if (controller.coupdoeilElement.openingPopover && !parentIsClosedOrClosing) {
await display(controller, options, beforeDisplay)
}
}
async function display(controller, options, beforeDisplay) {
if (controller.isOpen) return;
cancelClosingRequest(controller)
if (controller.card) {
controller.card.remove()
}
controller.card = buildPopoverElement(controller, options)
document.body.appendChild(controller.card)
if (options.animation) {
controller.card.dataset.animation = options.animation
}
executeOnNextFrameIfStillOpening(controller, async () => {
await positionPopover(controller.coupdoeilElement, controller.card, options)
// popover can be closed while waiting for positioning promise to resolve
if (controller.card === null) {
return clearPopover(controller)
}
// see buildPopoverElement() about next 2 lines
controller.card.classList.add('hidden')
controller.card.style.removeProperty('visibility')
executeOnNextFrameIfStillOpening(controller, async () => {
if (beforeDisplay) {
beforeDisplay(controller)
}
// // adding again the card to make sure it is in the map, could be better
addToCurrents(controller.coupdoeilElement)
delete controller.coupdoeilElement.openingPopover
controller.coupdoeilElement.dataset.popoverOpen = true
await enter(controller.card, 'popover')
})
})
}
function getDelayOptionsForController(controller, options) {
if (options.openingDelay === false || triggeredOnClick(controller)) {
return { fetch: 0, opening: 0 }
}
return { fetch: FETCH_DELAY_MS, opening: OPENING_DELAY_MS }
}
function buildPopoverElement(controller, options) {
const el = document.createElement('div')
el.setAttribute('role', 'dialog')
el.classList.add(POPOVER_CLASS_NAME)
el.style.cssText = 'position: absolute; left: 0; top: 0;'
el.innerHTML = getPopoverContentHTML(controller)
el.popoverController = controller
el.coupdoeilElement = controller.coupdoeilElement
el.close = function() { this.coupdoeilElement.closePopover() }
el.dataset.placement = options.placement
// Initial style is not .hidden (no display: none;) and visibility: 'hidden'; so the card is inserted
// in DOM the without being visible.
// This allows the browser to compute its actual size so the positioning is computed correctly.
el.style.visibility = 'hidden'
return el
}