motion-sensors-polyfill
Advanced tools
Comparing version 0.3.1 to 0.3.7
{ | ||
"name": "motion-sensors-polyfill", | ||
"version": "0.3.1", | ||
"version": "0.3.7", | ||
"description": "A polyfill for the motion sensors based on the W3C Generic Sensor API", | ||
@@ -27,3 +27,7 @@ "main": "motion-sensors.js", | ||
"url": "git+https://github.com/kenchris/sensor-polyfills.git" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^5.3.0", | ||
"eslint-config-google": "^0.9.1" | ||
} | ||
} |
W3C Generic Sensor API polyfills | ||
=== | ||
*Beware, we're still under active development. Expect rough edges.* | ||
This is a polyfill for [Generic Sensor](https://w3c.github.io/sensors/)-based [motions sensors](https://w3c.github.io/motion-sensors/) to make migration from the old [DeviceOrientationEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#deviceorientation)/[DeviceMotionEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#devicemotion) to the new APIs a smoother experience. | ||
In particular, this polyfill will allow the users of modern browsers to get a feel of the new API shape before it ships ([Chrome 63 has a native implementation](#how-to-enable-the-native-implementation-in-chrome)). | ||
In particular, this polyfill will allow the users of modern browsers to get a feel of the new API shape before it ships more broadly. | ||
@@ -20,2 +18,6 @@ `src/motion-sensors.js` implements the following interfaces: | ||
`src/geolocation.js` implements the following interface: | ||
- [`GeolocationSensor`](https://w3c.github.io/geolocation-sensor/#geolocationsensor-interface) | ||
How to use the polyfill | ||
@@ -44,31 +46,5 @@ === | ||
There are two ways: *Origin Trial* and *Enable via `chrome://flags`*. | ||
*Chrome 67 or later:* the native implementation is enabled by default. | ||
## Origin Trial | ||
Generic Sensor APIs are currently available as an [Origin Trial](https://bit.ly/OriginTrials) in Chrome 63+. | ||
To enable native Generic Sensor API implementation for all Chrome users on your site: | ||
1. Go to https://bit.ly/OriginTrialsSignup to get a token. | ||
2. Add the token to your web page as follows (replace `...` with your token): | ||
``` | ||
<!-- Origin Trial Token, feature = Generic Sensors, origin = https://example.org, expires ="2018-01-18" --> | ||
<meta http-equiv="origin-trial" data-feature="Generic Sensors" data-expires="2018-01-18" content="..."> | ||
``` | ||
3. Optional: add `motion-sensors.js` polyfill to cater for non-Chrome users (see [How to use the polyfill](#how-to-use-the-polyfill)). | ||
## Enable via `chrome://flags` | ||
The native implementation is behind the following feature flags in Chrome 63+: | ||
Generic Sensor (`chrome://flags/#enable-generic-sensor`): | ||
- `Accelerometer` | ||
- `Gyroscope` | ||
- `LinearAccelerationSensor` | ||
- `AbsoluteOrientationSensor` | ||
- `RelativeOrientationSensor` | ||
Generic Sensor Extra Classes (`chrome://flags/#enable-generic-sensor-extra-classes`): | ||
The Generic Sensor Extra Classes (`chrome://flags/#enable-generic-sensor-extra-classes`) feature flag can be activated to enable a few additional sensor types: | ||
- `AmbientLightSensor` | ||
@@ -82,7 +58,5 @@ - `Magnetometer` | ||
Known issues | ||
=== | ||
- `GravitySensor` and `LinearAccelerationSensor` polyfills do not work on Android with Pixel 2, since [`DeviceMotionEvent`](http://w3c.github.io/deviceorientation/spec-source-orientation.html#devicemotion_event)`.acceleration` returns only null values, see [Chromium bug 796518](https://crbug.com/796518). | ||
- `AbsoluteOrientationSensor` on iOS uses non-standard [`webkitCompassHeading`](https://developer.apple.com/documentation/webkitjs/deviceorientationevent/1804777-webkitcompassheading) that reports wrong readings if the device is held in its [`portrait-secondary`](https://w3c.github.io/screen-orientation/#dom-orientationtype-portrait-secondary) orientation. Specifically, the `webkitCompassHeading` flips by 180 degrees when tilted only slightly. | ||
@@ -93,2 +67,6 @@ | ||
- [Sensors For The Web article on Google's Web Fundaments](https://developers.google.com/web/updates/2017/09/sensors-for-the-web) - a web developer-oriented article explaining how to use the Generic Sensor-based APIs. | ||
- [Sensors For The Web article on Google's Web Fundaments](https://developers.google.com/web/updates/2017/09/sensors-for-the-web) - a web developer-oriented article explaining how to use the Generic Sensor-based APIs. | ||
Reporting a security issue | ||
=== | ||
If you have information about a security issue or vulnerability with an Intel-maintained open source project on https://github.com/intel, please send an e-mail to secure@intel.com. Encrypt sensitive information using our PGP public key. For issues related to Intel products, please visit https://security-center.intel.com. |
// @ts-check | ||
import { __sensor__, Sensor } from "./sensor.js"; | ||
import { | ||
__sensor__, | ||
Sensor, | ||
SensorErrorEvent, | ||
activateCallback, | ||
deactivateCallback, | ||
notifyActivatedState, | ||
notifyError, | ||
// AbortController, | ||
// AbortSignal, | ||
} from './sensor.js'; | ||
const slot = __sensor__; | ||
class GeolocationSensorSingleton { | ||
constructor() { | ||
if (!this.constructor.instance) { | ||
this.constructor.instance = this; | ||
} | ||
this.sensors = new Set(); | ||
this.watchId = null; | ||
this.accuracy = null; | ||
this.lastPosition = null; | ||
return this.constructor.instance; | ||
async function obtainPermission() { | ||
let state = 'prompt'; // Default for geolocation. | ||
// @ts-ignore | ||
if (navigator.permissions) { | ||
// @ts-ignore | ||
const permission = await navigator.permissions.query({name: 'geolocation'}); | ||
state = permission.state; | ||
} | ||
async obtainPermission() { | ||
let state = "prompt"; // Default for geolocation. | ||
// @ts-ignore | ||
if (navigator.permissions) { | ||
// @ts-ignore | ||
const permission = await navigator.permissions.query({ name:"geolocation"}); | ||
state = permission.state; | ||
return new Promise((resolve) => { | ||
if (state === 'granted') { | ||
return resolve(state); | ||
} | ||
return new Promise(resolve => { | ||
const successFn = position => { | ||
this.lastPosition = position; | ||
resolve("granted"); | ||
} | ||
const errorFn = err => { | ||
if (err.code === err.PERMISSION_DENIED) { | ||
resolve("denied"); | ||
} else { | ||
resolve(state); | ||
} | ||
const successFn = (_) => { | ||
resolve('granted'); | ||
}; | ||
const errorFn = (err) => { | ||
if (err.code === err.PERMISSION_DENIED) { | ||
resolve('denied'); | ||
} else { | ||
resolve(state); | ||
} | ||
}; | ||
const options = { maximumAge: Infinity, timeout: 10 }; | ||
navigator.geolocation.getCurrentPosition(successFn, errorFn, options); | ||
}); | ||
} | ||
const options = {maximumAge: Infinity, timeout: 0}; | ||
navigator.geolocation.getCurrentPosition(successFn, errorFn, options); | ||
}); | ||
} | ||
calculateAccuracy() { | ||
let enableHighAccuracy = false; | ||
function register(options, onreading, onerror, onactivated) { | ||
const handleEvent = (position) => { | ||
const timestamp = position.timestamp - performance.timing.navigationStart; | ||
const coords = position.coords; | ||
for (const sensor of this.sensors) { | ||
if (sensor[slot].options.accuracy === "high") { | ||
enableHighAccuracy = true; | ||
onreading(timestamp, coords); | ||
}; | ||
const handleError = (error) => { | ||
let type; | ||
switch (error.code) { | ||
case error.TIMEOUT: | ||
type = 'TimeoutError'; | ||
break; | ||
} | ||
case error.PERMISSION_DENIED: | ||
type = 'NotAllowedError'; | ||
break; | ||
case error.POSITION_UNAVAILABLE: | ||
type = 'NotReadableError'; | ||
break; | ||
default: | ||
type = 'UnknownError'; | ||
} | ||
return enableHighAccuracy; | ||
} | ||
onerror(error.message, type); | ||
}; | ||
async register(sensor) { | ||
const permission = await this.obtainPermission(); | ||
if (permission !== "granted") { | ||
sensor[slot].notifyError("Permission denied.", "NowAllowedError"); | ||
return; | ||
} | ||
const watchOptions = { | ||
enableHighAccuracy: false, | ||
maximumAge: 0, | ||
timeout: Infinity, | ||
}; | ||
if (this.lastPosition) { | ||
const age = performance.now() - this.lastPosition.timeStamp; | ||
const maxAge = sensor[slot].options.maxAge; | ||
if (maxAge == null || age <= maxAge) { | ||
sensor[slot].handleEvent(age, this.lastPosition.coords); | ||
} | ||
} | ||
const watchId = navigator.geolocation.watchPosition( | ||
handleEvent, handleError, watchOptions | ||
); | ||
this.sensors.add(sensor); | ||
return watchId; | ||
} | ||
// Check whether we need to reconfigure our navigation.geolocation | ||
// watch, ie. tear it down and recreate. | ||
const accuracy = this.calculateAccuracy(); | ||
if (this.watchId && this.accuracy === accuracy) { | ||
// We don't need to reset, return. | ||
return; | ||
} | ||
function deregister(watchId) { | ||
navigator.geolocation.clearWatch(watchId); | ||
} | ||
if (this.watchId) { | ||
navigator.geolocation.clearWatch(this.watchId); | ||
// Old geolocation API is FILO; on Chrome at least. | ||
class FIFOGeolocationEvents { | ||
constructor() { | ||
if (!this.constructor.instance) { | ||
this.constructor.instance = this; | ||
} | ||
const handleEvent = position => { | ||
this.lastPosition = position; | ||
// You can iterate through the elements of a map in insertion order. | ||
this.subscribers = new Map(); | ||
this.options = {}; | ||
this.watchId = null; | ||
const timestamp = position.timestamp - performance.timing.navigationStart; | ||
const coords = position.coords; | ||
this.lastReading = null; | ||
for (const sensor of this.sensors) { | ||
sensor[slot].handleEvent(timestamp, coords); | ||
} | ||
return this.constructor.instance; | ||
} | ||
unsubscribe(obj) { | ||
this.subscribers.delete(obj); | ||
if (!this.subscribers.size && this.watchId) { | ||
deregister(this.watchId); | ||
this.watchId = null; | ||
} | ||
} | ||
const handleError = error => { | ||
let type; | ||
switch(error.code) { | ||
case error.TIMEOUT: | ||
type = "TimeoutError"; | ||
break; | ||
case error.PERMISSION_DENIED: | ||
type = "NotAllowedError"; | ||
break; | ||
case error.POSITION_UNAVAILABLE: | ||
type = "NotReadableError"; | ||
break; | ||
default: | ||
type = "UnknownError"; | ||
subscribe(obj, options, onreading, onerror) { | ||
const fifoOnReading = (...args) => { | ||
this.lastReading = args; | ||
for ({onreading} of this.subscribers.values()) { | ||
if (typeof onreading === 'function') { | ||
onreading(...args); | ||
} | ||
} | ||
for (const sensor of this.sensors) { | ||
sensor[slot].handleError(error.message, type); | ||
}; | ||
const fifoOnError = (...args) => { | ||
for ({onerror} of this.subscribers.values()) { | ||
if (typeof onerror === 'function') { | ||
onerror(...args); | ||
} | ||
} | ||
} | ||
}; | ||
const options = { | ||
enableHighAccuracy: accuracy, | ||
maximumAge: 0, | ||
timeout: Infinity | ||
// TODO(spec): Generate the most precise options here | ||
// ie. lowest maximum-age and highest precision. | ||
this.options = options; | ||
if (this.watchId) { | ||
deregister(this.watchId); | ||
} | ||
this.watchId = navigator.geolocation.watchPosition( | ||
handleEvent, handleError, options | ||
// TODO(spec): Ensure lastReading is still valid. | ||
if (this.lastReading && typeof onreading === 'function') { | ||
onreading(...this.lastReading); | ||
} | ||
this.subscribers.set(obj, {onreading, onerror}); | ||
this.watchId = register(this.options, | ||
fifoOnReading, fifoOnError | ||
); | ||
} | ||
deregister(sensor) { | ||
this.sensors.delete(sensor); | ||
if (!this.sensors.size && this.watchId) { | ||
navigator.geolocation.clearWatch(this.watchId); | ||
this.watchId = null; | ||
} | ||
} | ||
} | ||
@@ -143,2 +154,48 @@ | ||
class GeolocationSensor extends Sensor { | ||
static async read(options = {}) { | ||
return new Promise(async (resolve, reject) => { | ||
const onerror = (message, name) => { | ||
let error = new SensorErrorEvent('error', { | ||
error: new DOMException(message, name), | ||
}); | ||
deregister(watchId); | ||
reject(error); | ||
}; | ||
const onreading = (timestamp, coords) => { | ||
const reading = { | ||
timestamp, | ||
accuracy: coords.accuracy, | ||
altitude: coords.altitude, | ||
altitudeAccuracy: coords.altitudeAccuracy, | ||
heading: coords.heading, | ||
latitude: coords.latitude, | ||
longitude: coords.longitude, | ||
speed: coords.speed, | ||
}; | ||
deregister(watchId); | ||
resolve(reading); | ||
}; | ||
const signal = options.signal; | ||
if (signal && signal.aborted) { | ||
return reject(new DOMException('Read was cancelled', 'AbortError')); | ||
} | ||
const permission = await obtainPermission(); | ||
if (permission !== 'granted') { | ||
onerror('Permission denied.', 'NowAllowedError'); | ||
return; | ||
} | ||
const watchId = register(options, onreading, onerror); | ||
if (signal) { | ||
signal.addEventListener('abort', () => { | ||
deregister(watchId); | ||
reject(new DOMException('Read was cancelled', 'AbortError')); | ||
}); | ||
} | ||
}); | ||
}; | ||
constructor(options = {}) { | ||
@@ -148,2 +205,3 @@ super(options); | ||
this[slot].options = options; | ||
this[slot].fifo = new FIFOGeolocationEvents(); | ||
@@ -157,18 +215,17 @@ const props = { | ||
heading: null, | ||
speed: null | ||
} | ||
speed: null, | ||
}; | ||
const propertyBag = this[slot]; | ||
/* eslint-disable-next-line guard-for-in */ | ||
for (const propName in props) { | ||
propertyBag[propName] = props[propName]; | ||
Object.defineProperty(this, propName, { | ||
get: () => propertyBag[propName] | ||
get: () => propertyBag[propName], | ||
}); | ||
} | ||
} | ||
this[slot].handleEvent = (timestamp, coords) => { | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
async [activateCallback]() { | ||
const onreading = (timestamp, coords) => { | ||
this[slot].timestamp = timestamp; | ||
@@ -185,17 +242,39 @@ | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
this.dispatchEvent(new Event('reading')); | ||
}; | ||
this[slot].handleError = (message, type) => { | ||
this[slot].notifyError(message, type); | ||
} | ||
const onerror = (message, type) => { | ||
this[notifyError](message, type); | ||
}; | ||
this[slot].activateCallback = () => { | ||
(new GeolocationSensorSingleton()).register(this); | ||
const permission = await obtainPermission(); | ||
if (permission !== 'granted') { | ||
onerror('Permission denied.', 'NowAllowedError'); | ||
return; | ||
} | ||
this[slot].deactivateCallback = () => { | ||
(new GeolocationSensorSingleton()).deregister(this); | ||
this[slot].fifo.subscribe( | ||
this, this[slot].options, | ||
onreading, onerror | ||
); | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
} | ||
} | ||
[deactivateCallback]() { | ||
this[slot].fifo.unsubscribe(this); | ||
this[slot].timestamp = null; | ||
this[slot].accuracy = null; | ||
this[slot].altitude = null; | ||
this[slot].altitudeAccuracy = null; | ||
this[slot].heading = null; | ||
this[slot].latitude = null; | ||
this[slot].longitude = null; | ||
this[slot].speed = null; | ||
this[slot].hasReading = false; | ||
} | ||
}; |
// @ts-check | ||
import { __sensor__, Sensor, defineReadonlyProperties } from "./sensor.js"; | ||
import { | ||
Sensor, | ||
defineReadonlyProperties, | ||
__sensor__, | ||
notifyError, | ||
notifyActivatedState, | ||
activateCallback, | ||
deactivateCallback, | ||
} from './sensor.js'; | ||
const slot = __sensor__; | ||
const handleEventCallback = Symbol('handleEvent'); | ||
@@ -16,7 +25,23 @@ let orientation; | ||
orientation = {}; | ||
Object.defineProperty(orientation, "angle", { | ||
get: () => { return (window.orientation || 0) } | ||
Object.defineProperty(orientation, 'angle', { | ||
get: () => { | ||
return (window.orientation || 0); | ||
}, | ||
}); | ||
} | ||
const rotationToRadian = (function() { | ||
// Returns Chrome version, or null if not Chrome. | ||
const match = navigator.userAgent.match(/.*Chrome\/([0-9]+)/); | ||
const chromeVersion = match ? parseInt(match[1], 10) : null; | ||
// DeviceMotion/Orientation API return deg/s (except Chrome, | ||
// but fixing in M66). Gyroscope needs rad/s. | ||
const returnsDegrees = chromeVersion === null || chromeVersion >= 66; | ||
const conversion = returnsDegrees ? Math.PI / 180 : 1.0; | ||
return function(value) { | ||
return value * conversion; | ||
}; | ||
})(); | ||
const DeviceOrientationMixin = (superclass, ...eventNames) => class extends superclass { | ||
@@ -33,10 +58,10 @@ constructor(...args) { | ||
} | ||
} | ||
this[slot].activateCallback = () => { | ||
window.addEventListener(this[slot].eventName, this[slot].handleEvent, { capture: true }); | ||
} | ||
[activateCallback]() { | ||
window.addEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
window.removeEventListener(this[slot].eventName, this[slot].handleEvent, { capture: true }); | ||
} | ||
[deactivateCallback]() { | ||
window.removeEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); | ||
} | ||
@@ -46,3 +71,3 @@ }; | ||
function toQuaternionFromEuler(alpha, beta, gamma) { | ||
const degToRad = Math.PI / 180 | ||
const degToRad = Math.PI / 180; | ||
@@ -76,3 +101,3 @@ const x = (beta || 0) * degToRad; | ||
axis[2] * sHalfAngle, | ||
cHalfAngle | ||
cHalfAngle, | ||
]; | ||
@@ -95,3 +120,3 @@ | ||
return quat.map(v => v / length); | ||
return quat.map((v) => v / length); | ||
} | ||
@@ -130,3 +155,3 @@ | ||
function worldToScreen(quaternion) { | ||
function deviceToScreen(quaternion) { | ||
return !quaternion ? null : | ||
@@ -142,50 +167,50 @@ rotateQuaternionByAxisAngle( | ||
export const RelativeOrientationSensor = window.RelativeOrientationSensor || | ||
class RelativeOrientationSensor extends DeviceOrientationMixin(Sensor, "deviceorientation") { | ||
class RelativeOrientationSensor extends DeviceOrientationMixin(Sensor, 'deviceorientation') { | ||
constructor(options = {}) { | ||
super(options); | ||
switch (options.coordinateSystem || 'world') { | ||
switch (options.referenceFrame || 'device') { | ||
case 'screen': | ||
Object.defineProperty(this, "quaternion", { | ||
get: () => worldToScreen(this[slot].quaternion) | ||
Object.defineProperty(this, 'quaternion', { | ||
get: () => deviceToScreen(this[slot].quaternion), | ||
}); | ||
break; | ||
case 'world': | ||
default: | ||
Object.defineProperty(this, "quaternion", { | ||
get: () => this[slot].quaternion | ||
default: // incl. case 'device' | ||
Object.defineProperty(this, 'quaternion', { | ||
get: () => this[slot].quaternion, | ||
}); | ||
} | ||
} | ||
this[slot].handleEvent = event => { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.absolute || event.alpha === null) { | ||
// Spec: The implementation can still decide to provide | ||
// absolute orientation if relative is not available or | ||
// the resulting data is more accurate. In either case, | ||
// the absolute property must be set accordingly to reflect | ||
// the choice. | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
[handleEventCallback](event) { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.absolute || event.alpha === null) { | ||
// Spec: The implementation can still decide to provide | ||
// absolute orientation if relative is not available or | ||
// the resulting data is more accurate. In either case, | ||
// the absolute property must be set accordingly to reflect | ||
// the choice. | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].timestamp = performance.now(); | ||
this[slot].quaternion = toQuaternionFromEuler( | ||
event.alpha, | ||
event.beta, | ||
event.gamma | ||
); | ||
this[slot].quaternion = toQuaternionFromEuler( | ||
event.alpha, | ||
event.beta, | ||
event.gamma | ||
); | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].quaternion = null; | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].quaternion = null; | ||
} | ||
@@ -196,3 +221,3 @@ | ||
} | ||
} | ||
}; | ||
@@ -202,54 +227,54 @@ // @ts-ignore | ||
class AbsoluteOrientationSensor extends DeviceOrientationMixin( | ||
Sensor, "deviceorientationabsolute", "deviceorientation") { | ||
Sensor, 'deviceorientationabsolute', 'deviceorientation') { | ||
constructor(options = {}) { | ||
super(options); | ||
switch (options.coordinateSystem || 'world') { | ||
switch (options.referenceFrame || 'device') { | ||
case 'screen': | ||
Object.defineProperty(this, "quaternion", { | ||
get: () => worldToScreen(this[slot].quaternion) | ||
Object.defineProperty(this, 'quaternion', { | ||
get: () => deviceToScreen(this[slot].quaternion), | ||
}); | ||
break; | ||
case 'world': | ||
default: | ||
Object.defineProperty(this, "quaternion", { | ||
get: () => this[slot].quaternion | ||
default: // incl. case 'device' | ||
Object.defineProperty(this, 'quaternion', { | ||
get: () => this[slot].quaternion, | ||
}); | ||
} | ||
} | ||
this[slot].handleEvent = event => { | ||
// If absolute is set, or webkitCompassHeading exists, | ||
// absolute values should be available. | ||
const isAbsolute = event.absolute === true || "webkitCompassHeading" in event; | ||
const hasValue = event.alpha !== null || event.webkitCompassHeading !== undefined; | ||
[handleEventCallback](event) { | ||
// If absolute is set, or webkitCompassHeading exists, | ||
// absolute values should be available. | ||
const isAbsolute = event.absolute === true || 'webkitCompassHeading' in event; | ||
const hasValue = event.alpha !== null || event.webkitCompassHeading !== undefined; | ||
if (!isAbsolute || !hasValue) { | ||
// Spec: If an implementation can never provide absolute | ||
// orientation information, the event should be fired with | ||
// the alpha, beta and gamma attributes set to null. | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
if (!isAbsolute || !hasValue) { | ||
// Spec: If an implementation can never provide absolute | ||
// orientation information, the event should be fired with | ||
// the alpha, beta and gamma attributes set to null. | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].hasReading = true; | ||
this[slot].timestamp = performance.now(); | ||
this[slot].hasReading = true; | ||
this[slot].timestamp = performance.now(); | ||
const heading = event.webkitCompassHeading != null ? 360 - event.webkitCompassHeading : event.alpha; | ||
const heading = event.webkitCompassHeading != null ? 360 - event.webkitCompassHeading : event.alpha; | ||
this[slot].quaternion = toQuaternionFromEuler( | ||
heading, | ||
event.beta, | ||
event.gamma | ||
); | ||
this[slot].quaternion = toQuaternionFromEuler( | ||
heading, | ||
event.beta, | ||
event.gamma | ||
); | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].quaternion = null; | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].quaternion = null; | ||
} | ||
@@ -260,162 +285,166 @@ | ||
} | ||
} | ||
}; | ||
// @ts-ignore | ||
export const Gyroscope = window.Gyroscope || | ||
class Gyroscope extends DeviceOrientationMixin(Sensor, "devicemotion") { | ||
class Gyroscope extends DeviceOrientationMixin(Sensor, 'devicemotion') { | ||
constructor(options) { | ||
super(options); | ||
this[slot].handleEvent = event => { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.rotationRate.alpha === null) { | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.rotationRate.alpha; | ||
this[slot].y = event.rotationRate.beta; | ||
this[slot].z = event.rotationRate.gamma; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
defineReadonlyProperties(this, slot, { | ||
x: null, | ||
y: null, | ||
z: null | ||
z: null, | ||
}); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
[handleEventCallback](event) { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.rotationRate.alpha === null) { | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = rotationToRadian(event.rotationRate.alpha); | ||
this[slot].y = rotationToRadian(event.rotationRate.beta); | ||
this[slot].z = rotationToRadian(event.rotationRate.gamma); | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
} | ||
}; | ||
// @ts-ignore | ||
export const Accelerometer = window.Accelerometer || | ||
class Accelerometer extends DeviceOrientationMixin(Sensor, "devicemotion") { | ||
class Accelerometer extends DeviceOrientationMixin(Sensor, 'devicemotion') { | ||
constructor(options) { | ||
super(options); | ||
this[slot].handleEvent = event => { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.accelerationIncludingGravity.x === null) { | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.accelerationIncludingGravity.x; | ||
this[slot].y = event.accelerationIncludingGravity.y; | ||
this[slot].z = event.accelerationIncludingGravity.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
defineReadonlyProperties(this, slot, { | ||
x: null, | ||
y: null, | ||
z: null | ||
z: null, | ||
}); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
[handleEventCallback](event) { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.accelerationIncludingGravity.x === null) { | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.accelerationIncludingGravity.x; | ||
this[slot].y = event.accelerationIncludingGravity.y; | ||
this[slot].z = event.accelerationIncludingGravity.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
} | ||
}; | ||
// @ts-ignore | ||
export const LinearAccelerationSensor = window.LinearAccelerationSensor || | ||
class LinearAccelerationSensor extends DeviceOrientationMixin(Sensor, "devicemotion") { | ||
class LinearAccelerationSensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { | ||
constructor(options) { | ||
super(options); | ||
this[slot].handleEvent = event => { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.acceleration.x === null) { | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.acceleration.x; | ||
this[slot].y = event.acceleration.y; | ||
this[slot].z = event.acceleration.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
defineReadonlyProperties(this, slot, { | ||
x: null, | ||
y: null, | ||
z: null | ||
z: null, | ||
}); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
[handleEventCallback](event) { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.acceleration.x === null) { | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.acceleration.x; | ||
this[slot].y = event.acceleration.y; | ||
this[slot].z = event.acceleration.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
} | ||
}; | ||
// @ts-ignore | ||
export const GravitySensor = window.GravitySensor || | ||
class GravitySensor extends DeviceOrientationMixin(Sensor, "devicemotion") { | ||
class GravitySensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { | ||
constructor(options) { | ||
super(options); | ||
this[slot].handleEvent = event => { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.acceleration.x === null || event.accelerationIncludingGravity.x === null) { | ||
this[slot].notifyError("Could not connect to a sensor", "NotReadableError"); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[slot].notifyActivatedState(); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.accelerationIncludingGravity.x - event.acceleration.x; | ||
this[slot].y = event.accelerationIncludingGravity.y - event.acceleration.y; | ||
this[slot].z = event.accelerationIncludingGravity.z - event.acceleration.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event("reading")); | ||
} | ||
defineReadonlyProperties(this, slot, { | ||
x: null, | ||
y: null, | ||
z: null | ||
z: null, | ||
}); | ||
} | ||
this[slot].deactivateCallback = () => { | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
[handleEventCallback](event) { | ||
// If there is no sensor we will get values equal to null. | ||
if (event.acceleration.x === null || event.accelerationIncludingGravity.x === null) { | ||
this[notifyError]('Could not connect to a sensor', 'NotReadableError'); | ||
return; | ||
} | ||
if (!this[slot].activated) { | ||
this[notifyActivatedState](); | ||
} | ||
this[slot].timestamp = performance.now(); | ||
this[slot].x = event.accelerationIncludingGravity.x - event.acceleration.x; | ||
this[slot].y = event.accelerationIncludingGravity.y - event.acceleration.y; | ||
this[slot].z = event.accelerationIncludingGravity.z - event.acceleration.z; | ||
this[slot].hasReading = true; | ||
this.dispatchEvent(new Event('reading')); | ||
} | ||
} | ||
[deactivateCallback]() { | ||
super[deactivateCallback](); | ||
this[slot].x = null; | ||
this[slot].y = null; | ||
this[slot].z = null; | ||
} | ||
}; |
// @ts-check | ||
export const __sensor__ = Symbol("__sensor__"); | ||
const slot = __sensor__; | ||
function defineProperties(target, descriptions) { | ||
/* eslint-disable-next-line guard-for-in */ | ||
for (const property in descriptions) { | ||
Object.defineProperty(target, property, { | ||
configurable: true, | ||
value: descriptions[property] | ||
value: descriptions[property], | ||
}); | ||
@@ -15,2 +13,4 @@ } | ||
const privates = new WeakMap(); | ||
export const EventTargetMixin = (superclass, ...eventNames) => class extends superclass { | ||
@@ -21,44 +21,104 @@ constructor(...args) { | ||
const eventTarget = document.createDocumentFragment(); | ||
privates.set(this, eventTarget); | ||
} | ||
this.addEventListener = (type, ...args) => { | ||
return eventTarget.addEventListener(type, ...args); | ||
addEventListener(type, ...args) { | ||
const eventTarget = privates.get(this); | ||
return eventTarget.addEventListener(type, ...args); | ||
} | ||
removeEventListener(...args) { | ||
const eventTarget = privates.get(this); | ||
// @ts-ignore | ||
return eventTarget.removeEventListener(...args); | ||
} | ||
dispatchEvent(event) { | ||
defineProperties(event, {currentTarget: this}); | ||
if (!event.target) { | ||
defineProperties(event, {target: this}); | ||
} | ||
this.removeEventListener = (...args) => { | ||
// @ts-ignore | ||
return eventTarget.removeEventListener(...args); | ||
const eventTarget = privates.get(this); | ||
const retValue = eventTarget.dispatchEvent(event); | ||
if (retValue && this.parentNode) { | ||
this.parentNode.dispatchEvent(event); | ||
} | ||
this.dispatchEvent = (event) => { | ||
defineProperties(event, { currentTarget: this }); | ||
if (!event.target) { | ||
defineProperties(event, { target: this }); | ||
} | ||
defineProperties(event, {currentTarget: null, target: null}); | ||
return retValue; | ||
} | ||
}; | ||
export class EventTarget extends EventTargetMixin(Object) {} | ||
const __abort__ = Symbol('__abort__'); | ||
export class AbortSignal extends EventTarget { | ||
constructor() { | ||
super(); | ||
this[__abort__] = { | ||
aborted: false, | ||
}; | ||
defineOnEventListener(this, 'abort'); | ||
Object.defineProperty(this, 'aborted', { | ||
get: () => this[__abort__].aborted, | ||
}); | ||
} | ||
dispatchEvent(event) { | ||
if (event.type === 'abort') { | ||
this[__abort__].aborted = true; | ||
const methodName = `on${event.type}`; | ||
if (typeof this[methodName] == "function") { | ||
if (typeof this[methodName] == 'function') { | ||
this[methodName](event); | ||
} | ||
} | ||
super.dispatchEvent(event); | ||
} | ||
const retValue = eventTarget.dispatchEvent(event); | ||
toString() { | ||
return '[object AbortSignal]'; | ||
} | ||
} | ||
if (retValue && this.parentNode) { | ||
this.parentNode.dispatchEvent(event); | ||
} | ||
export class AbortController { | ||
constructor() { | ||
const signal = new AbortSignal(); | ||
Object.defineProperty(this, 'signal', { | ||
get: () => signal, | ||
}); | ||
} | ||
defineProperties(event, { currentTarget: null, target: null }); | ||
abort() { | ||
let abort = new Event('abort'); | ||
this.signal.dispatchEvent(abort); | ||
} | ||
return retValue; | ||
} | ||
toString() { | ||
return '[object AbortController]'; | ||
} | ||
}; | ||
} | ||
export class EventTarget extends EventTargetMixin(Object) {}; | ||
function defineOnEventListener(target, name) { | ||
Object.defineProperty(target, `on${name}`, { | ||
enumerable: true, | ||
configurable: false, | ||
writable: true, | ||
value: null, | ||
}); | ||
} | ||
export function defineReadonlyProperties(target, slot, descriptions) { | ||
const propertyBag = target[slot]; | ||
/* eslint-disable-next-line guard-for-in */ | ||
for (const property in descriptions) { | ||
propertyBag[property] = descriptions[property]; | ||
Object.defineProperty(target, property, { | ||
get: () => propertyBag[property] | ||
get: () => propertyBag[property], | ||
}); | ||
@@ -68,3 +128,3 @@ } | ||
class SensorErrorEvent extends Event { | ||
export class SensorErrorEvent extends Event { | ||
constructor(type, errorEventInitDict) { | ||
@@ -75,22 +135,13 @@ super(type, errorEventInitDict); | ||
throw TypeError( | ||
"Failed to construct 'SensorErrorEvent':" + | ||
"2nd argument much contain 'error' property" | ||
'Failed to construct \'SensorErrorEvent\':' + | ||
'2nd argument much contain \'error\' property' | ||
); | ||
} | ||
Object.defineProperty(this, "error", { | ||
Object.defineProperty(this, 'error', { | ||
configurable: false, | ||
writable: false, | ||
value: errorEventInitDict.error | ||
value: errorEventInitDict.error, | ||
}); | ||
} | ||
}; | ||
function defineOnEventListener(target, name) { | ||
Object.defineProperty(target, `on${name}`, { | ||
enumerable: true, | ||
configurable: false, | ||
writable: true, | ||
value: null | ||
}); | ||
} | ||
@@ -102,46 +153,68 @@ | ||
ACTIVE: 3, | ||
} | ||
}; | ||
export const __sensor__ = Symbol('__sensor__'); | ||
const slot = __sensor__; | ||
export const notifyError = Symbol('Sensor.notifyError'); | ||
export const notifyActivatedState = Symbol('Sensor.notifyActivatedState'); | ||
export const activateCallback = Symbol('Sensor.activateCallback'); | ||
export const deactivateCallback = Symbol('Sensor.deactivateCallback'); | ||
export class Sensor extends EventTarget { | ||
[activateCallback]() {} | ||
[deactivateCallback]() {} | ||
[notifyError](message, name) { | ||
let error = new SensorErrorEvent('error', { | ||
error: new DOMException(message, name), | ||
}); | ||
this.dispatchEvent(error); | ||
this.stop(); | ||
} | ||
[notifyActivatedState]() { | ||
let activate = new Event('activate'); | ||
this[slot].activated = true; | ||
this.dispatchEvent(activate); | ||
this[slot].state = SensorState.ACTIVE; | ||
} | ||
constructor(options) { | ||
super(); | ||
this[slot] = new WeakMap; | ||
defineOnEventListener(this, "reading"); | ||
defineOnEventListener(this, "activate"); | ||
defineOnEventListener(this, "error"); | ||
this[__sensor__] = { | ||
// Internal slots | ||
state: SensorState.IDLE, | ||
frequency: null, | ||
defineReadonlyProperties(this, slot, { | ||
// Property backing | ||
activated: false, | ||
hasReading: false, | ||
timestamp: null | ||
}) | ||
timestamp: null, | ||
}; | ||
this[slot].state = SensorState.IDLE; | ||
defineOnEventListener(this, 'reading'); | ||
defineOnEventListener(this, 'activate'); | ||
defineOnEventListener(this, 'error'); | ||
this[slot].notifyError = (message, name) => { | ||
let error = new SensorErrorEvent("error", { | ||
error: new DOMException(message, name) | ||
}); | ||
this.dispatchEvent(error); | ||
this.stop(); | ||
} | ||
Object.defineProperty(this, 'activated', { | ||
get: () => this[slot].activated, | ||
}); | ||
Object.defineProperty(this, 'hasReading', { | ||
get: () => this[slot].hasReading, | ||
}); | ||
Object.defineProperty(this, 'timestamp', { | ||
get: () => this[slot].timestamp, | ||
}); | ||
this[slot].notifyActivatedState = () => { | ||
let activate = new Event("activate"); | ||
this[slot].activated = true; | ||
this.dispatchEvent(activate); | ||
this[slot].state = SensorState.ACTIVE; | ||
} | ||
this[slot].activateCallback = () => {}; | ||
this[slot].deactivateCallback = () => {}; | ||
this[slot].frequency = null; | ||
if (window && window.parent != window.top) { | ||
throw new DOMException("Only instantiable in a top-level browsing context", "SecurityError"); | ||
throw new DOMException( | ||
'Only instantiable in a top-level browsing context', | ||
'SecurityError' | ||
); | ||
} | ||
if (options && typeof(options.frequency) == "number") { | ||
if (options && typeof(options.frequency) == 'number') { | ||
if (options.frequency > 60) { | ||
@@ -153,8 +226,27 @@ this.frequency = options.frequency; | ||
dispatchEvent(event) { | ||
switch (event.type) { | ||
case 'reading': | ||
case 'error': | ||
case 'activate': | ||
{ | ||
const methodName = `on${event.type}`; | ||
if (typeof this[methodName] == 'function') { | ||
this[methodName](event); | ||
} | ||
super.dispatchEvent(event); | ||
break; | ||
} | ||
default: | ||
super.dispatchEvent(event); | ||
} | ||
} | ||
start() { | ||
if (this[slot].state === SensorState.ACTIVATING || this[slot].state === SensorState.ACTIVE) { | ||
if (this[slot].state === SensorState.ACTIVATING || | ||
this[slot].state === SensorState.ACTIVE) { | ||
return; | ||
} | ||
this[slot].state = SensorState.ACTIVATING; | ||
this[slot].activateCallback(); | ||
this[activateCallback](); | ||
} | ||
@@ -169,6 +261,6 @@ | ||
this[slot].timestamp = null; | ||
this[slot].deactivateCallback(); | ||
this[deactivateCallback](); | ||
this[slot].state = SensorState.IDLE; | ||
} | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
31583
2
6
814
69
1