feat: docker compose maybe

This commit is contained in:
2023-11-13 16:10:04 -05:00
parent 180b261e40
commit b625ccd8d6
8031 changed files with 2182966 additions and 0 deletions

View File

@ -0,0 +1,7 @@
import { type Writable } from 'svelte/store';
import type { PopupSettings } from './types.js';
export declare const storePopup: Writable<any>;
export declare function popup(triggerNode: HTMLElement, args: PopupSettings): {
update(newArgs: PopupSettings): void;
destroy(): void;
};

View File

@ -0,0 +1,237 @@
import { get, writable } from 'svelte/store';
// Use a store to pass the Floating UI import references
export const storePopup = writable(undefined);
export function popup(triggerNode, args) {
// Floating UI Modules
const { computePosition, autoUpdate, offset, shift, flip, arrow, size, autoPlacement, hide, inline } = get(storePopup);
// Local State
const popupState = {
open: false,
autoUpdateCleanup: () => { }
};
const focusableAllowedList = ':is(a[href], button, input, textarea, select, details, [tabindex]):not([tabindex="-1"])';
let focusablePopupElements;
const documentationLink = 'https://www.skeleton.dev/utilities/popups';
// Elements
let elemPopup;
let elemArrow;
function setDomElements() {
elemPopup = document.querySelector(`[data-popup="${args.target}"]`) ?? document.createElement('div');
elemArrow = elemPopup.querySelector(`.arrow`) ?? document.createElement('div');
}
setDomElements(); // init
// Render Floating UI Popup
function render() {
// Error handling for required Floating UI modules
if (!elemPopup)
throw new Error(`The data-popup="${args.target}" element was not found. ${documentationLink}`);
if (!computePosition)
throw new Error(`Floating UI 'computePosition' not found for data-popup="${args.target}". ${documentationLink}`);
if (!offset)
throw new Error(`Floating UI 'offset' not found for data-popup="${args.target}". ${documentationLink}`);
if (!shift)
throw new Error(`Floating UI 'shift' not found for data-popup="${args.target}". ${documentationLink}`);
if (!flip)
throw new Error(`Floating UI 'flip' not found for data-popup="${args.target}". ${documentationLink}`);
if (!arrow)
throw new Error(`Floating UI 'arrow' not found for data-popup="${args.target}". ${documentationLink}`);
// Bundle optional middleware
const optionalMiddleware = [];
// https://floating-ui.com/docs/size
if (size)
optionalMiddleware.push(size(args.middleware?.size));
// https://floating-ui.com/docs/autoPlacement
if (autoPlacement)
optionalMiddleware.push(autoPlacement(args.middleware?.autoPlacement));
// https://floating-ui.com/docs/hide
if (hide)
optionalMiddleware.push(hide(args.middleware?.hide));
// https://floating-ui.com/docs/inline
if (inline)
optionalMiddleware.push(inline(args.middleware?.inline));
// Floating UI Compute Position
// https://floating-ui.com/docs/computePosition
computePosition(triggerNode, elemPopup, {
placement: args.placement ?? 'bottom',
// Middleware - NOTE: the order matters:
// https://floating-ui.com/docs/middleware#ordering
middleware: [
// https://floating-ui.com/docs/offset
offset(args.middleware?.offset ?? 8),
// https://floating-ui.com/docs/shift
shift(args.middleware?.shift ?? { padding: 8 }),
// https://floating-ui.com/docs/flip
flip(args.middleware?.flip),
// https://floating-ui.com/docs/arrow
arrow(args.middleware?.arrow ?? { element: elemArrow || null }),
// Implement optional middleware
...optionalMiddleware
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(elemPopup.style, {
left: `${x}px`,
top: `${y}px`
});
// Handle Arrow Placement:
// https://floating-ui.com/docs/arrow
if (elemArrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
// @ts-expect-error implicit any
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement.split('-')[0]];
Object.assign(elemArrow.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px'
});
}
});
}
// State Handlers
function open() {
if (!elemPopup)
return;
// Set open state to on
popupState.open = true;
// Return the current state
if (args.state)
args.state({ state: popupState.open });
// Update render settings
render();
// Update the DOM
elemPopup.style.display = 'block';
elemPopup.style.opacity = '1';
elemPopup.style.pointerEvents = 'auto';
// enable popup interactions
elemPopup.removeAttribute('inert');
// Trigger Floating UI autoUpdate (open only)
// https://floating-ui.com/docs/autoUpdate
popupState.autoUpdateCleanup = autoUpdate(triggerNode, elemPopup, render);
// Focus the first focusable element within the popup
focusablePopupElements = Array.from(elemPopup?.querySelectorAll(focusableAllowedList));
}
function close(callback) {
if (!elemPopup)
return;
// Set transition duration
const cssTransitionDuration = parseFloat(window.getComputedStyle(elemPopup).transitionDuration.replace('s', '')) * 1000;
setTimeout(() => {
// Set open state to off
popupState.open = false;
// Return the current state
if (args.state)
args.state({ state: popupState.open });
// Update the DOM
elemPopup.style.opacity = '0';
// disable popup interactions
elemPopup.setAttribute('inert', '');
// Cleanup Floating UI autoUpdate (close only)
if (popupState.autoUpdateCleanup)
popupState.autoUpdateCleanup();
// Trigger callback
if (callback)
callback();
}, cssTransitionDuration);
}
// Event Handlers
function toggle() {
popupState.open === false ? open() : close();
}
function onWindowClick(event) {
// Return if the popup is not yet open
if (popupState.open === false)
return;
// Return if click is the trigger element
if (triggerNode.contains(event.target))
return;
// If click it outside the popup
if (elemPopup && elemPopup.contains(event.target) === false) {
close();
return;
}
// Handle Close Query State
const closeQueryString = args.closeQuery === undefined ? 'a[href], button' : args.closeQuery;
const closableMenuElements = elemPopup?.querySelectorAll(closeQueryString);
closableMenuElements?.forEach((elem) => {
if (elem.contains(event.target))
close();
});
}
// Keyboard Interactions for A11y
const onWindowKeyDown = (event) => {
if (popupState.open === false)
return;
// Handle keys
const key = event.key;
// On Esc key
if (key === 'Escape') {
event.preventDefault();
triggerNode.focus();
close();
return;
}
// Update focusable elements (important for Autocomplete)
focusablePopupElements = Array.from(elemPopup?.querySelectorAll(focusableAllowedList));
// On Tab or ArrowDown key
const triggerMenuFocused = popupState.open && document.activeElement === triggerNode;
if (triggerMenuFocused &&
(key === 'ArrowDown' || key === 'Tab') &&
focusableAllowedList.length > 0 &&
focusablePopupElements.length > 0) {
event.preventDefault();
focusablePopupElements[0].focus();
}
};
// Event Listeners
switch (args.event) {
case 'click':
triggerNode.addEventListener('click', toggle, true);
window.addEventListener('click', onWindowClick, true);
break;
case 'hover':
triggerNode.addEventListener('mouseover', open, true);
triggerNode.addEventListener('mouseleave', () => close(), true);
break;
case 'focus-blur':
triggerNode.addEventListener('focus', toggle, true);
triggerNode.addEventListener('blur', () => close(), true);
break;
case 'focus-click':
triggerNode.addEventListener('focus', open, true);
window.addEventListener('click', onWindowClick, true);
break;
default:
throw new Error(`Event value of '${args.event}' is not supported. ${documentationLink}`);
}
window.addEventListener('keydown', onWindowKeyDown, true);
// Render popup on initialization
render();
// Lifecycle
return {
update(newArgs) {
close(() => {
args = newArgs;
render();
setDomElements();
});
},
destroy() {
// Trigger Events
triggerNode.removeEventListener('click', toggle, true);
triggerNode.removeEventListener('mouseover', open, true);
triggerNode.removeEventListener('mouseleave', () => close(), true);
triggerNode.removeEventListener('focus', toggle, true);
triggerNode.removeEventListener('focus', open, true);
triggerNode.removeEventListener('blur', () => close(), true);
// Window Events
window.removeEventListener('click', onWindowClick, true);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
};
}

View File

@ -0,0 +1,40 @@
/** Placement https://floating-ui.com/docs/computePosition#placement */
type Direction = 'top' | 'bottom' | 'left' | 'right';
type Placement = Direction | `${Direction}-start` | `${Direction}-end`;
export interface Middleware {
/** Offset middleware settings: https://floating-ui.com/docs/offset */
offset?: number | Record<string, any>;
/** Shift middleware settings: https://floating-ui.com/docs/shift */
shift?: Record<string, any>;
/** Flip middleware settings: https://floating-ui.com/docs/flip */
flip?: Record<string, any>;
/** Arrow middleware settings: https://floating-ui.com/docs/arrow */
arrow?: {
element: string;
} & Record<string, any>;
/** Size middleware settings: https://floating-ui.com/docs/size */
size?: Record<string, any>;
/** Auto Placement middleware settings: https://floating-ui.com/docs/autoPlacement */
autoPlacement?: Record<string, any>;
/** Hide middleware settings: https://floating-ui.com/docs/hide */
hide?: Record<string, any>;
/** Inline middleware settings: https://floating-ui.com/docs/inline */
inline?: Record<string, any>;
}
export interface PopupSettings {
/** Provide the event type. */
event: 'click' | 'hover' | 'focus-blur' | 'focus-click';
/** Match the popup data value `data-popup="targetNameHere"` */
target: string;
/** Set the placement position. Defaults 'bottom'. */
placement?: Placement;
/** Query elements that close the popup when clicked. Defaults `'a[href], button'`. */
closeQuery?: string;
/** Optional callback function that reports state change. */
state?: (event: {
state: boolean;
}) => void;
/** Provide Floating UI middleware settings. */
middleware?: Middleware;
}
export {};

View File

@ -0,0 +1,2 @@
// Popup Types
export {};