@fleetbase/ember-ui
Version:
Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.
259 lines (215 loc) • 7.96 kB
JavaScript
import { getOwner } from '@ember/application';
import { DEBUG } from '@glimmer/env';
import { warn } from '@ember/debug';
import { schedule } from '@ember/runloop';
import { isArray } from '@ember/array';
import { all } from 'rsvp';
import requirejs from 'require';
function childNodesOfElement(element) {
let children = [];
let child = element.firstChild;
while (child) {
children.push(child);
child = child.nextSibling;
}
return children;
}
export function findElementById(doc, id) {
if (doc.getElementById) {
return doc.getElementById(id);
}
let nodes = childNodesOfElement(doc);
let node;
while (nodes.length) {
node = nodes.shift();
if (node.getAttribute && node.getAttribute('id') === id) {
return node;
}
nodes = childNodesOfElement(node).concat(nodes);
}
}
// Private Ember API usage. Get the dom implementation used by the current
// renderer, be it native browser DOM or Fastboot SimpleDOM
export function getDOM(context) {
let { renderer } = context;
if (!renderer?._dom) {
// pre glimmer2
let container = getOwner ? getOwner(context) : context.container;
let documentService = container.lookup('service:-document');
if (documentService) {
return documentService;
}
renderer = container.lookup('renderer:-dom');
}
if (renderer._dom && renderer._dom.document) {
return renderer._dom.document;
} else {
throw new Error('Could not get DOM');
}
}
export function getDestinationElement(context) {
let dom = getDOM(context);
const id = 'ember-bootstrap-wormhole';
let destinationElement = findElementById(dom, id) || findElemementByIdInShadowDom(context, id);
if (DEBUG && !destinationElement) {
let config = getOwner(context).resolveRegistration('config:environment');
if (config.environment === 'test' && typeof FastBoot === 'undefined') {
let id;
if (requirejs.has('@ember/test-helpers/dom/get-root-element')) {
try {
id = requirejs('@ember/test-helpers/dom/get-root-element').default().id;
} catch (ex) {
// no op
}
}
if (!id) {
return document.querySelector('#ember-testing');
}
return document.getElementById(id);
}
warn(
`No wormhole destination element found for component ${context}. If you have set \`insertEmberWormholeElementToDom\` to false, you should insert a \`div#ember-bootstrap-wormhole\` manually!`,
false,
{ id: 'ember-bootstrap.no-destination-element' }
);
}
return destinationElement;
}
export function findElemementByIdInShadowDom(context, id) {
const owner = getOwner(context);
return owner.rootElement.querySelector && owner.rootElement.querySelector(`[id="${id}"]`);
}
export function unwrapChildren(context) {
const fragment = document.createDocumentFragment();
while (context.firstChild) {
fragment.appendChild(context.firstChild);
}
context.parentNode.insertBefore(fragment, context);
context.parentNode.removeChild(context);
}
export function afterRender() {
return new Promise((resolve) => schedule('afterRender', null, resolve));
}
export function afterPaint() {
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}
export function renderCompleted() {
return all([afterRender, afterPaint]);
}
export function waitForInsertedAndSized(getElOrEl, { timeoutMs = 4000 } = {}) {
const getEl = typeof getElOrEl === 'function' ? getElOrEl : () => getElOrEl;
return new Promise((resolve, reject) => {
const ok = (el) => {
if (!el) return false;
const inDoc = document.documentElement.contains(el);
if (!inDoc) return false;
const r = el.getBoundingClientRect?.();
return !!r && r.width > 0 && r.height > 0;
};
let toId = 0;
let mo = null;
const check = () => {
const el = getEl();
if (ok(el)) {
cleanup();
resolve(el);
}
};
function cleanup() {
if (mo) mo.disconnect();
if (toId) clearTimeout(toId);
}
// fast path
if (ok(getEl())) return resolve(getEl());
// observe the whole doc for insertions/attribute tweaks that would affect layout
mo = new MutationObserver(check);
mo.observe(document.documentElement, {
childList: true,
attributes: true,
subtree: true,
});
// one immediate tick to catch “already there but styles settling”
requestAnimationFrame(check);
if (timeoutMs >= 0) {
toId = setTimeout(() => {
cleanup();
reject(new Error('Element was not inserted/sized in time'));
}, timeoutMs);
}
});
}
/**
* Create a DOM element with declarative options.
*
* @param {string} tag
* @param {Object} [options]
* @param {string|string[]|Node|Node[]} [children]
* @returns {HTMLElement}
*/
export function createElement(tag, options = {}, children = null) {
const el = document.createElement(tag);
// ---------- Classes ----------
if (options.classNames) {
const classes = isArray(options.classNames) ? options.classNames : options.classNames.split(' ');
el.classList.add(...classes.filter(Boolean));
}
// ---------- Styles ----------
if (options.styles && typeof options.styles === 'object') {
Object.assign(el.style, options.styles);
}
// ---------- Attributes ----------
if (options.attrs && typeof options.attrs === 'object') {
for (const [key, value] of Object.entries(options.attrs)) {
if (value !== false && value != null) {
el.setAttribute(key, value === true ? '' : value);
}
}
}
// ---------- Dataset ----------
if (options.dataset && typeof options.dataset === 'object') {
for (const [key, value] of Object.entries(options.dataset)) {
el.dataset[key] = value;
}
}
// ---------- Event listeners ----------
if (options.on && typeof options.on === 'object') {
for (const [event, handler] of Object.entries(options.on)) {
if (typeof handler === 'function') {
el.addEventListener(event, handler);
}
}
}
// ---------- Text / HTML (exclusive) ----------
const hasText = options.text != null || options.innerText != null;
const hasHtml = options.html != null || options.innerHTML != null;
if (hasText && hasHtml) {
throw new Error('createElement: use either text OR html, not both.');
}
if (hasText) {
el.textContent = options.text ?? options.innerText;
} else if (hasHtml) {
el.innerHTML = options.html ?? options.innerHTML;
} else {
// ---------- Children ----------
const append = (child) => {
if (child == null) return;
if (Array.isArray(child)) return child.forEach(append);
if (child instanceof Node) el.appendChild(child);
else el.appendChild(document.createTextNode(String(child)));
};
append(children);
}
// ---------- Mount ----------
if (options.mount) {
let mountTarget = options.mount;
if (typeof mountTarget === 'string') {
mountTarget = document.querySelector(mountTarget);
}
if (mountTarget instanceof Element) {
mountTarget.appendChild(el);
} else {
console.warn('createElement: mount target not found', options.mount);
}
}
return el;
}