open-consul/ui-v2/app/services/dom.js

163 lines
5.1 KiB
JavaScript

import Service from '@ember/service';
import { getOwner } from '@ember/application';
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 Service.extend({
doc: document,
win: window,
init: function() {
this._super(...arguments);
inViewportCallbacks = new WeakMap();
$_ = getComponentFactory(getOwner(this));
},
willDestroy: function() {
this._super(...arguments);
inViewportCallbacks = null;
$_ = null;
},
document: function() {
return this.doc;
},
viewport: function() {
return this.win;
},
guid: function(el) {
return guidFor(el);
},
// TODO: should this be here? Needs a better name at least
clickFirstAnchor: clickFirstAnchor,
closest: closest,
sibling: sibling,
isOutside: isOutside,
normalizeEvent: normalizeEvent,
setEventTargetProperty: function(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: function(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: function() {
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: function(id) {
return this.doc.getElementById(id);
},
elementsByTagName: function(name, context) {
context = typeof context === 'undefined' ? this.doc : context;
return context.getElementsByTagName(name);
},
elements: function(selector, context) {
// don't ever be tempted to [...$$()] here
// it should return a NodeList
return $$(selector, context);
},
element: function(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: function(selector, context) {
if (typeof selector !== 'string') {
return $_(selector);
}
return $_(this.element(selector, context));
},
components: function(selector, context) {
return [...this.elements(selector, context)]
.map(function(item) {
return $_(item);
})
.filter(function(item) {
return item != null;
});
},
isInViewport: function($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;
};
},
});