UNPKG

coupdoeil

Version:

Javascript for Ruby on Rails Coupdoeil gem

150 lines (127 loc) 5.25 kB
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 }