200 lines
5.4 KiB
JavaScript
200 lines
5.4 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
|
|
import Service, { inject as service } from '@ember/service';
|
|
import { guidFor } from '@ember/object/internals';
|
|
|
|
// selecting
|
|
import qsaFactory from 'consul-ui/utils/dom/qsa-factory';
|
|
// TODO: sibling and closest seem to have 'PHP-like' guess the order arguments
|
|
// ie. one `string, element` and the other has `element, string`
|
|
// see if its possible to standardize
|
|
import sibling from 'consul-ui/utils/dom/sibling';
|
|
import closest from 'consul-ui/utils/dom/closest';
|
|
import isOutside from 'consul-ui/utils/dom/is-outside';
|
|
import getComponentFactory from 'consul-ui/utils/dom/get-component-factory';
|
|
|
|
// events
|
|
import normalizeEvent from 'consul-ui/utils/dom/normalize-event';
|
|
import createListeners from 'consul-ui/utils/dom/create-listeners';
|
|
import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor';
|
|
|
|
// ember-eslint doesn't like you using a single $ so use double
|
|
// use $_ for components
|
|
const $$ = qsaFactory();
|
|
let $_;
|
|
let inViewportCallbacks;
|
|
const clickFirstAnchor = clickFirstAnchorFactory(closest);
|
|
export default class DomService extends Service {
|
|
@service('-document') doc;
|
|
|
|
constructor(owner) {
|
|
super(...arguments);
|
|
inViewportCallbacks = new WeakMap();
|
|
$_ = getComponentFactory(owner);
|
|
}
|
|
|
|
willDestroy() {
|
|
super.willDestroy(...arguments);
|
|
inViewportCallbacks = null;
|
|
$_ = null;
|
|
}
|
|
|
|
document() {
|
|
return this.doc;
|
|
}
|
|
|
|
viewport() {
|
|
return this.doc.defaultView;
|
|
}
|
|
|
|
guid(el) {
|
|
return guidFor(el);
|
|
}
|
|
|
|
focus($el) {
|
|
if (typeof $el === 'string') {
|
|
$el = this.element($el);
|
|
}
|
|
if (typeof $el !== 'undefined') {
|
|
let previousIndex = $el.getAttribute('tabindex');
|
|
$el.setAttribute('tabindex', '0');
|
|
$el.focus();
|
|
if (previousIndex === null) {
|
|
$el.removeAttribute('tabindex');
|
|
} else {
|
|
$el.setAttribute('tabindex', previousIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: should this be here? Needs a better name at least
|
|
clickFirstAnchor = clickFirstAnchor;
|
|
|
|
closest = closest;
|
|
sibling = sibling;
|
|
isOutside = isOutside;
|
|
normalizeEvent = normalizeEvent;
|
|
|
|
setEventTargetProperty(e, property, cb) {
|
|
const target = e.target;
|
|
return new Proxy(e, {
|
|
get: function (obj, prop, receiver) {
|
|
if (prop === 'target') {
|
|
return new Proxy(target, {
|
|
get: function (obj, prop, receiver) {
|
|
if (prop === property) {
|
|
return cb(e.target[property]);
|
|
}
|
|
return target[prop];
|
|
},
|
|
});
|
|
}
|
|
return Reflect.get(...arguments);
|
|
},
|
|
});
|
|
}
|
|
|
|
setEventTargetProperties(e, propObj) {
|
|
const target = e.target;
|
|
return new Proxy(e, {
|
|
get: function (obj, prop, receiver) {
|
|
if (prop === 'target') {
|
|
return new Proxy(target, {
|
|
get: function (obj, prop, receiver) {
|
|
if (typeof propObj[prop] !== 'undefined') {
|
|
return propObj[prop](e.target);
|
|
}
|
|
return target[prop];
|
|
},
|
|
});
|
|
}
|
|
return Reflect.get(...arguments);
|
|
},
|
|
});
|
|
}
|
|
|
|
listeners = createListeners;
|
|
|
|
root() {
|
|
return this.doc.documentElement;
|
|
}
|
|
|
|
// TODO: Should I change these to use the standard names
|
|
// even though they don't have a standard signature (querySelector*)
|
|
elementById(id) {
|
|
return this.doc.getElementById(id);
|
|
}
|
|
|
|
elementsByTagName(name, context) {
|
|
context = typeof context === 'undefined' ? this.doc : context;
|
|
return context.getElementsByTagName(name);
|
|
}
|
|
|
|
elements(selector, context) {
|
|
// don't ever be tempted to [...$$()] here
|
|
// it should return a NodeList
|
|
return $$(selector, context);
|
|
}
|
|
|
|
element(selector, context) {
|
|
if (selector.substr(0, 1) === '#') {
|
|
return this.elementById(selector.substr(1));
|
|
}
|
|
// TODO: This can just use querySelector
|
|
return [...$$(selector, context)][0];
|
|
}
|
|
|
|
// ember components aren't strictly 'dom-like'
|
|
// but if you think of them as a web component 'shim'
|
|
// then it makes more sense to think of them as part of the dom
|
|
// with traditional/standard web components you wouldn't actually need this
|
|
// method as you could just get to their methods from the dom element
|
|
component(selector, context) {
|
|
if (typeof selector !== 'string') {
|
|
return $_(selector);
|
|
}
|
|
return $_(this.element(selector, context));
|
|
}
|
|
|
|
components(selector, context) {
|
|
return [...this.elements(selector, context)]
|
|
.map(function (item) {
|
|
return $_(item);
|
|
})
|
|
.filter(function (item) {
|
|
return item != null;
|
|
});
|
|
}
|
|
|
|
isInViewport($el, cb, threshold = 0) {
|
|
inViewportCallbacks.set($el, cb);
|
|
let observer = new IntersectionObserver(
|
|
(entries, observer) => {
|
|
entries.map((item) => {
|
|
const cb = inViewportCallbacks.get(item.target);
|
|
if (typeof cb === 'function') {
|
|
cb(item.isIntersecting);
|
|
}
|
|
});
|
|
},
|
|
{
|
|
rootMargin: '0px',
|
|
threshold: threshold,
|
|
}
|
|
);
|
|
observer.observe($el); // eslint-disable-line ember/no-observers
|
|
// observer.unobserve($el);
|
|
return () => {
|
|
observer.unobserve($el); // eslint-disable-line ember/no-observers
|
|
if (inViewportCallbacks) {
|
|
inViewportCallbacks.delete($el);
|
|
}
|
|
observer.disconnect(); // eslint-disable-line ember/no-observers
|
|
observer = null;
|
|
};
|
|
}
|
|
}
|