initial implementation

This commit is contained in:
Preet Shihn
2020-04-18 14:50:03 -07:00
parent 85e8b8fa63
commit 3cfac78f62
7 changed files with 579 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
lib
bin
demo

4
.npmignore Normal file
View File

@@ -0,0 +1,4 @@
demo
node_modules
tsconfig.json
bin

279
package-lock.json generated Normal file
View File

@@ -0,0 +1,279 @@
{
"name": "planar-range",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
"dev": true,
"requires": {
"@babel/highlight": "^7.8.3"
}
},
"@babel/helper-validator-identifier": {
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
"integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==",
"dev": true
},
"@babel/highlight": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
"integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.9.0",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@types/node": {
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz",
"integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
},
"fsevents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
"integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"jest-worker": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz",
"integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==",
"dev": true,
"requires": {
"merge-stream": "^2.0.0",
"supports-color": "^6.1.0"
},
"dependencies": {
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"pointer-tracker": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.3.0.tgz",
"integrity": "sha512-2tQaxrI62yNRIE6jFkxCpa/nraqavDWPZH1WVVAEA9TlH7CWjqX6sfCsWwrWeSYcQwAiZ/l3YfqXNDp4aQIOCQ==",
"dev": true
},
"resolve": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.1.tgz",
"integrity": "sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.6.1.tgz",
"integrity": "sha512-1RhFDRJeg027YjBO6+JxmVWkEZY0ASztHhoEUEWxOwkh4mjO58TFD6Uo7T7Y3FbmDpRTfKhM5NVxJyimCn0Elg==",
"dev": true,
"requires": {
"fsevents": "~2.1.2"
}
},
"rollup-plugin-node-resolve": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
"integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
"dev": true,
"requires": {
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.11.1",
"rollup-pluginutils": "^2.8.1"
}
},
"rollup-plugin-terser": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz",
"integrity": "sha512-XGMJihTIO3eIBsVGq7jiNYOdDMb3pVxuzY0uhOE/FM4x/u9nQgr3+McsjzqBn3QfHIpNSZmFnpoKAwHBEcsT7g==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"jest-worker": "^24.9.0",
"rollup-pluginutils": "^2.8.2",
"serialize-javascript": "^2.1.2",
"terser": "^4.6.2"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
}
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"terser": {
"version": "4.6.11",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz",
"integrity": "sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
}
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
}
}
}

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "planar-range",
"version": "0.1.0",
"description": "A 2D range component ",
"main": "lib/planar-range.min.js",
"module": "lib/planar-range.min.js",
"types": "lib/planar-range.d.ts",
"scripts": {
"build": "rm -rf lib && tsc && rollup -c",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pshihn/planar-range.git"
},
"keywords": [
"range",
"webcomponent"
],
"author": "Preet Shihn",
"license": "MIT",
"bugs": {
"url": "https://github.com/pshihn/planar-range/issues"
},
"homepage": "https://github.com/pshihn/planar-range#readme",
"devDependencies": {
"pointer-tracker": "^2.3.0",
"rollup": "^2.6.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.3.0",
"typescript": "^3.8.3"
}
}

13
rollup.config.js Normal file
View File

@@ -0,0 +1,13 @@
import resolve from 'rollup-plugin-node-resolve';
import { terser } from "rollup-plugin-terser";
export default [
{
input: 'lib/planar-range.js',
output: {
file: 'lib/planar-range.min.js',
format: 'esm'
},
plugins: [resolve(), terser()]
}
];

221
src/planar-range.ts Normal file
View File

@@ -0,0 +1,221 @@
import PointerTracker from 'pointer-tracker';
export interface PlanarRangeThumbValue {
name?: string;
x: number;
y: number;
}
function fire(element: HTMLElement, name: string, detail?: any) {
if (name) {
const init: CustomEventInit = {
bubbles: true,
composed: true
};
if (detail) {
init.detail = detail;
}
element.dispatchEvent(new CustomEvent(name, init));
}
}
export class PlanarRangeThumb extends HTMLElement {
private _x = 1;
private _y = 1;
static get observedAttributes() { return ['x', 'y']; }
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
:host {
display: block;
width: 10px;
height: 10px;
box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 1px 3px 0 rgba(0,0,0,.12);
border-radius: 50%;
background: white;
border: 1px solid #e5e5e5;
transform: translate3d(-50%, -50%, 0);
cursor: pointer;
position: absolute;
}
</style>
`;
}
attributeChangedCallback(name: string, _: string, newValue: string) {
if (name === 'x') {
this.x = +newValue;
} else if (name === 'y') {
this.y = +newValue;
}
}
connectedCallback() {
this.updatePosition();
}
private updatePosition() {
(new Promise(() => {
this.style.left = `${this._x * 100}%`;
this.style.top = `${this._y * 100}%`;
}));
}
get x(): number {
return this._x;
}
get y(): number {
return this._y;
}
set x(value: number) {
value = Math.max(0, Math.min(1, value || 0));
if (value !== this._x) {
this._x = value;
this.updatePosition();
}
}
set y(value: number) {
value = Math.max(0, Math.min(1, value || 0));
if (value !== this._y) {
this._y = value;
this.updatePosition();
}
}
get value(): [number, number] {
return [this._x, this._y];
}
set value(c: [number, number]) {
this.x = c[0];
this.y = c[1];
}
setValue(v: [number, number], fireEvent: boolean) {
const [oldx, oldy] = [this._x, this._y];
this.value = v;
if (fireEvent && (oldx !== this._x || oldy !== this._y)) {
fire(this, 'change', {
name: this.getAttribute('name') || undefined,
x: this._x,
y: this._y
});
}
}
}
export class PlanarRange extends HTMLElement {
private root: ShadowRoot;
private _slot?: HTMLSlotElement;
private _container?: HTMLDivElement;
private thumbs: PlanarRangeThumb[] = [];
private pointerMap = new Map<PlanarRangeThumb, PointerTracker>();
constructor() {
super();
this.root = this.attachShadow({ mode: 'open' });
this.root.innerHTML = `
<style>
:host {
display: inline-block;
width: 200px;
height: 200px;
border: 1px solid #d8d8d8;
}
#container {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
}
#container planar-range-thumb,
::slotted(planar-range-thumb) {
position: absolute;
}
</style>
<div id="container"><slot></slot></div>
`
}
private get slotElement(): HTMLSlotElement {
if (!this._slot) {
this._slot = this.root.querySelector('slot')!;
}
return this._slot;
}
private get container(): HTMLDivElement {
if (!this._container) {
this._container = this.root.querySelector('#container') as HTMLDivElement;
}
return this._container;
}
private updateThumbs() {
const nodes = this.slotElement.assignedNodes().filter((n) => {
return (n.nodeType === Node.ELEMENT_NODE) && ((n as HTMLElement).tagName.toLowerCase() === 'planar-range-thumb');
});
const thumbs: PlanarRangeThumb[] = [];
nodes.forEach((n) => {
const t = n as PlanarRangeThumb;
if (this.thumbs.indexOf(t) < 0) {
let viewAnchor = [0, 0, 0, 0];
const tracker = new PointerTracker(t, {
start: (_, event) => {
event.preventDefault();
const rect = this.container.getBoundingClientRect();
viewAnchor = [rect.top || rect.x, rect.left || rect.y, rect.width, rect.height];;
return true;
},
move: (_, changedPointers) => {
const pointer = changedPointers[0];
if (pointer) {
const w = viewAnchor[2];
const h = viewAnchor[3];
t.value = [
w ? ((pointer.pageX - viewAnchor[0]) / w) : 0,
h ? ((pointer.pageY - viewAnchor[1]) / h) : 0
];
}
}
});
this.pointerMap.set(t, tracker);
}
thumbs.push(t);
});
this.thumbs = thumbs;
}
connectedCallback() {
this.slotElement.addEventListener('slotchange', () => this.updateThumbs());
this.updateThumbs();
}
disconnectedCallback() {
for (const tracker of this.pointerMap.values()) {
tracker.stop();
}
this.pointerMap.clear();
}
get values(): PlanarRangeThumbValue[] {
return this.thumbs.map<PlanarRangeThumbValue>((thumb) => {
return {
x: thumb.x,
y: thumb.y,
name: thumb.getAttribute('name') || undefined
};
});
}
}
customElements.define('planar-range-thumb', PlanarRangeThumb);
customElements.define('planar-range', PlanarRange);

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es2017",
"module": "es2015",
"moduleResolution": "node",
"resolveJsonModule": true,
"lib": [
"es2017",
"dom"
],
"declaration": true,
"outDir": "./lib",
"baseUrl": ".",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*.ts"
]
}