coupdoeil
Version:
Javascript for Ruby on Rails Coupdoeil gem
150 lines (127 loc) • 5.25 kB
JavaScript
import {FETCH_DELAY_MS, POPOVER_CLASS_NAME, OPENING_DELAY_MS} from "./config"
import {getParams, getType, 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 {cancelCloseRequest, clear as clearPopover} from "./closing"
function fetchPopoverContent(controller) {
const type = getType(controller)
const params = getParams(controller)
const authenticityToken = document.querySelector('meta[name=csrf-token]')?.content
let url = `/coupdoeil/popover`
const opts = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ params, action_name: type, authenticity_token: authenticityToken })
}
return fetch(url, opts)
.then((response) => {
if (response.status >= 400) {
throw 'error while fetching popover content'
}
return response.text()
})
}
async function loadPopoverContentHTML(controller, options, delayOptions) {
return new Promise((resolve) => {
setTimeout(async () => {
if (!controller.coupdoeilElement.openingPopover) return // opening has been canceled
if (options.cache === false || (options.cache && !getPopoverContentHTML(controller))) {
let html
if (options.loading === "preload") {
html = preloadedContentElement(controller).innerHTML
} else {
html = await fetchPopoverContent(controller)
}
setPopoverContentHTML(controller, html)
}
resolve()
}, delayOptions.fetch)
})
}
export async function openPopover(controller, { parent, beforeDisplay }) {
if (controller.isOpen) {
return cancelCloseRequest(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;
cancelCloseRequest(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
}
executeNextFrameIfStillOpening(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')
executeNextFrameIfStillOpening(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 executeNextFrameIfStillOpening(controller, callback) {
requestAnimationFrame(() => {
if (controller.card && controller.coupdoeilElement.openingPopover && !controller.closingRequest) {
callback.call()
} else {
clearPopover(controller)
}
})
}
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
}