@vaadin-component-factory/vcf-slider
Advanced tools
Comparing version 1.0.5 to 1.0.6
142
package.json
{ | ||
"name": "@vaadin-component-factory/vcf-slider", | ||
"version": "1.0.5", | ||
"version": "1.0.6", | ||
"description": "Slider web component for the Vaadin platform.", | ||
"main": "dist/vcf-slider.js", | ||
"module": "dist/vcf-slider.js", | ||
"exports": { | ||
".": "./dist/vcf-slider.js", | ||
"./vcf-slider.js": "./dist/src/vcf-slider.js" | ||
}, | ||
"main": "out-tsc/vcf-slider.js", | ||
"module": "out-tsc/vcf-slider.js", | ||
"types": "out-tsc/vcf-slider.d.ts", | ||
"type": "module", | ||
"author": "Vaadin Ltd", | ||
@@ -15,70 +13,94 @@ "license": "Apache-2.0", | ||
"type": "git", | ||
"url": "git+https://github.com/vaadin/vcf-slider.git" | ||
"url": "git+https://github.com/vaadin-component-factory/vcf-slider.git" | ||
}, | ||
"files": [ | ||
"out-tsc", | ||
"src", | ||
"vcf-slider.ts" | ||
], | ||
"keywords": [ | ||
"Vaadin", | ||
"lit", | ||
"typescript", | ||
"vaadin", | ||
"vaadin-component-factory", | ||
"web-component", | ||
"vcf-slider", | ||
"lit-element" | ||
"web-component" | ||
], | ||
"scripts": { | ||
"start": "run-p build:watch start:dev", | ||
"start:dev": "wds --app-index demo/index.html --node-resolve --open --watch", | ||
"prestart": "npm run analyze", | ||
"analyze": "wca analyze \"src/**/*.ts\" --outFile demo/custom-elements.json", | ||
"build": "tsc", | ||
"build:demo": "NODE_ENV=production rollup -c", | ||
"build:prod": "run-s analyze build:demo", | ||
"build:watch": "tsc --watch", | ||
"analyze": "cem analyze --litelement --globs \"src/**/*.ts\" --outdir demo", | ||
"analyze:watch": "run-s \"analyze -- --watch\"", | ||
"build": "run-s clean analyze build:compile", | ||
"build:bundle": "rollup -c", | ||
"build:compile": "tsc", | ||
"build:prod": "run-s build build:bundle", | ||
"build:watch": "run-s \"build:compile -- --watch\"", | ||
"clean": "rimraf build out-tsc demo/custom-elements.json", | ||
"format": "run-s format:**", | ||
"format:eslint": "eslint --ext .ts,.html . --fix --ignore-path .gitignore", | ||
"format:prettier": "prettier \"**/*.js\" \"**/*.ts\" --write --ignore-path .gitignore", | ||
"lint": "run-s lint:**", | ||
"format": "run-s format:**", | ||
"lint:eslint": "eslint --ext .ts,.html . --ignore-path .gitignore", | ||
"format:eslint": "eslint --ext .ts,.html . --fix --ignore-path .gitignore", | ||
"lint:prettier": "prettier \"**/*.js\" \"**/*.ts\" --check --ignore-path .gitignore", | ||
"format:prettier": "prettier \"**/*.js\" \"**/*.ts\" --write --ignore-path .gitignore", | ||
"test": "run-s build \"web-test-runner --coverage\"", | ||
"test:watch": "web-test-runner --watch", | ||
"prepublishOnly": "run-s build analyze", | ||
"publish": "node util/publish.js", | ||
"prepublish": "tsc" | ||
"serve": "wds --app-index build/index.html --node-resolve --open --watch", | ||
"start": "run-p analyze:watch build:watch start:dev", | ||
"start:dev": "wds --app-index demo/index.html --node-resolve --open --watch", | ||
"test": "run-s build:compile \"test:run -- --coverage\"", | ||
"test:dev": "run-p \"build:watch -- --preserveWatchOutput\" \"test:run -- --watch\"", | ||
"test:run": "wtr", | ||
"test:watch": "run-s build:compile test:dev" | ||
}, | ||
"dependencies": { | ||
"lit": "^2.0.0" | ||
"@vaadin/vaadin-themable-mixin": "^23.2.10", | ||
"lit": "^2.4.1" | ||
}, | ||
"devDependencies": { | ||
"@open-wc/building-rollup": "^1.9.4", | ||
"@open-wc/eslint-config": "^4.0.0", | ||
"@open-wc/testing": "^2.0.0", | ||
"@types/node": "13.11.1", | ||
"@typescript-eslint/eslint-plugin": "^2.20.0", | ||
"@typescript-eslint/parser": "^2.20.0", | ||
"@vaadin-component-factory/vcf-anchor-nav": "^1.1.0", | ||
"@vaadin-component-factory/vcf-element-util": "0.2.1-beta", | ||
"@web/dev-server": "^0.0.12", | ||
"@web/test-runner": "^0.7.41", | ||
"@webcomponents/webcomponentsjs": "^2.0.0", | ||
"api-viewer-element": "0.5.0", | ||
"eslint": "^6.1.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"husky": "^1.0.0", | ||
"lint-staged": "^10.0.0", | ||
"@api-viewer/demo": "^1.0.0-pre.6", | ||
"@api-viewer/docs": "^1.0.0-pre.6", | ||
"@custom-elements-manifest/analyzer": "^0.6.6", | ||
"@open-wc/building-rollup": "^2.2.1", | ||
"@open-wc/testing": "^3.1.6", | ||
"@typescript-eslint/eslint-plugin": "^5.42.1", | ||
"@typescript-eslint/parser": "^5.42.1", | ||
"@vaadin-component-factory/vcf-anchor-nav": "^23.2.1", | ||
"@vaadin-component-factory/vcf-element-util": "^0.3.5", | ||
"@vaadin/vaadin-lumo-styles": "^23.2.8", | ||
"@web/dev-server": "^0.1.34", | ||
"@web/test-runner": "^0.14.0", | ||
"@webcomponents/webcomponentsjs": "^2.7.0", | ||
"concurrently": "^5.3.0", | ||
"deepmerge": "^4.2.2", | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"husky": "^4.3.8", | ||
"lint-staged": "^10.5.4", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^2.0.4", | ||
"rollup-plugin-copy": "^3.3.0", | ||
"tslib": "^1.11.0", | ||
"typescript": "~4.0.3", | ||
"web-component-analyzer": "^1.1.6" | ||
"prettier": "^2.4.1", | ||
"rimraf": "^3.0.2", | ||
"rollup-plugin-copy": "^3.4.0", | ||
"tslib": "^2.3.1", | ||
"typescript": "^4.5.2" | ||
}, | ||
"customElements": "demo/custom-elements.json", | ||
"eslintConfig": { | ||
"parser": "@typescript-eslint/parser", | ||
"extends": [ | ||
"@open-wc/eslint-config", | ||
"eslint-config-prettier" | ||
] | ||
"prettier" | ||
], | ||
"plugins": [ | ||
"@typescript-eslint" | ||
], | ||
"rules": { | ||
"@typescript-eslint/no-unused-vars": [ | ||
"error" | ||
] | ||
}, | ||
"env": { | ||
"browser": true, | ||
"es6": true | ||
} | ||
}, | ||
"prettier": { | ||
"singleQuote": true, | ||
"arrowParens": "avoid", | ||
"trailingComma": "none", | ||
"printWidth": 120 | ||
"arrowParens": "avoid" | ||
}, | ||
@@ -93,11 +115,5 @@ "husky": { | ||
"eslint --fix", | ||
"prettier --write", | ||
"git add" | ||
"prettier --write" | ||
] | ||
}, | ||
"files": [ | ||
"vcf-slider.ts", | ||
"src", | ||
"dist" | ||
] | ||
} | ||
} |
@@ -1,14 +0,6 @@ | ||
import { html, css, PropertyValues, LitElement } from 'lit'; | ||
import { query, property, state, customElement } from 'lit/decorators'; | ||
import { html, css, PropertyValues, LitElement, render, TemplateResult } from 'lit'; | ||
import { query, property, customElement } from 'lit/decorators.js'; | ||
import { CustomEventMixin, CustomEvents, ValueChangedEvent } from './mixins/CustomEventMixin'; | ||
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin'; | ||
type PointerEvent = MouseEvent | TouchEvent; | ||
// const TOUCH_DEVICE = (() => { | ||
// try { | ||
// document.createEvent('TouchEvent'); | ||
// return true; | ||
// } catch (e) { | ||
// return false; | ||
// } | ||
// })(); | ||
/** | ||
@@ -21,7 +13,23 @@ * `<vcf-slider>` Slider web component for the Vaadin platform. | ||
* @csspart knob-n - Nth knob element. | ||
* | ||
* @cssprop [--vcf-slider-knob-alt-color=var(--lumo-error-color)] - Color of `::part(alt-knob)`. | ||
* @cssprop [--vcf-slider-knob-color=var(--lumo-primary-color)] - Color of `::part(knob)`. | ||
* @cssprop [--vcf-slider-knob-size=var(--lumo-space-m)] - Size (width, height) of `::part(knob)`. | ||
* @cssprop [--vcf-slider-label-font-size=var(--lumo-font-size-s)] - Font size of `::part(label)`. | ||
* @cssprop [--vcf-slider-line-alt-color=var(--lumo-contrast-30pct)] - Secondary background color of `::part(line)`. | ||
* @cssprop [--vcf-slider-line-color=var(--lumo-contrast-50pct)] - Background color of `::part(line)`. | ||
* @cssprop [--vcf-slider-line-height=var(--lumo-space-s)] - Width of `::part(line)`. | ||
* @cssprop [--vcf-slider-padding=var(--lumo-space-xs)] - Padding of `::part(container)`. | ||
* @cssprop [--vcf-slider-width=100%] - Width of `:host`. | ||
* | ||
* @event {ValueChangedEvent} value-changed - Fired when the slider value changes. Returns a single knob value and index. | ||
*/ | ||
@customElement('vcf-slider') | ||
export class VcfSlider extends LitElement { | ||
@property({ type: Boolean, reflect: true }) labels = true; | ||
@property({ type: Number }) value: number | number[] = 0; | ||
export class Slider extends CustomEventMixin(ThemableMixin(LitElement)) { | ||
/** | ||
* | ||
*/ | ||
@property({ type: Boolean, reflect: true }) labels = false; | ||
@property({ type: Boolean, reflect: true }) vertical = false; | ||
@property({ type: Number }) value: string | number | number[] = 0; | ||
@property({ type: Number }) ranges = 0; | ||
@@ -31,40 +39,75 @@ @property({ type: Number }) step = 1; | ||
@property({ type: Number }) max = 100; | ||
@state() private knobs = 1; | ||
// private touchDevice = TOUCH_DEVICE; | ||
@query('#knobs') private knobsContainer?: HTMLElement; | ||
@query('#line') private line?: HTMLElement; | ||
@query('#line-color') private lineColorElement?: HTMLElement; | ||
protected static is() { | ||
return 'vcf-slider'; | ||
} | ||
protected static get version() { | ||
return '1.0.6'; | ||
} | ||
private knob?: HTMLElement; | ||
private originalKnobOffsetX = 0; | ||
private originalPointerX = 0; | ||
private originalKnobOffsetXY = 0; | ||
private originalPointerXY: number | null = 0; | ||
private knobCount = 1; | ||
@query('[part="knobs"]') private knobsContainer?: HTMLElement; | ||
@query('[part="line"]') private line?: HTMLElement; | ||
@query('[part="line-color"]') private lineColor?: HTMLElement; | ||
private get xy() { | ||
return this.vertical ? 'y' : 'x'; | ||
} | ||
static get version() { | ||
return '1.0.5'; | ||
private get pageXY() { | ||
return this.vertical ? 'pageY' : 'pageX'; | ||
} | ||
private set knobs(value: number) { | ||
this.knobCount = value > 0 ? value : 1; | ||
} | ||
private get knobs() { | ||
return this.knobCount; | ||
} | ||
static get styles() { | ||
return css` | ||
:host { | ||
display: block; | ||
display: flex; | ||
margin: var(--lumo-space-s) 0; | ||
--vcf-slider-padding: var(--lumo-space-xs); | ||
--vcf-slider-line-width: calc(100% - 2 * var(--vcf-slider-padding)); | ||
--vcf-slider-line-height: var(--lumo-space-s); | ||
width: var(--vcf-slider-width); | ||
/* PUBLIC */ | ||
--vcf-slider-knob-alt-color: var(--lumo-error-color); | ||
--vcf-slider-knob-color: var(--lumo-primary-color); | ||
--vcf-slider-knob-size: var(--lumo-space-m); | ||
--vcf-slider-label-font-size: var(--lumo-font-size-s); | ||
--vcf-slider-line-alt-color: var(--lumo-contrast-30pct); | ||
--vcf-slider-line-color: var(--lumo-contrast-50pct); | ||
--vcf-slider-line-alternate-color: var(--lumo-contrast-30pct); | ||
--vs-l-height: var(--vcf-slider-line-height); | ||
--vs-k-size: var(--vcf-slider-knob-size); | ||
--vcf-slider-line-height: var(--lumo-space-s); | ||
--vcf-slider-padding: var(--lumo-space-xs); | ||
--vcf-slider-width: 100%; | ||
--vcf-slider-vertical-height: 200px; | ||
/* PRIVATE */ | ||
--l-height: var(--vcf-slider-line-height); | ||
--k-size: calc(var(--vcf-slider-knob-size) * 2); | ||
} | ||
[part='container'] { | ||
width: var(--vcf-slider-line-width); | ||
:host * { | ||
box-sizing: border-box; | ||
} | ||
:host([labels]) { | ||
padding-top: calc(var(--vcf-slider-label-font-size) + 4px + var(--lumo-space-s)); | ||
} | ||
#container { | ||
width: 100%; | ||
padding: var(--vcf-slider-padding); | ||
} | ||
[part='line'] { | ||
#line { | ||
position: relative; | ||
width: 100%; | ||
height: var(--vcf-slider-line-height); | ||
height: var(--l-height); | ||
border-radius: var(--lumo-border-radius-m); | ||
@@ -74,3 +117,3 @@ background-color: var(--lumo-contrast-30pct); | ||
[part='line-color'] { | ||
#line-color { | ||
position: absolute; | ||
@@ -86,3 +129,11 @@ top: 0; | ||
position: absolute; | ||
top: calc(-0.5 * var(--vs-k-size) + calc(0.5 * var(--vs-l-height))); | ||
display: flex; | ||
top: calc(-0.5 * var(--k-size) + calc(0.5 * var(--l-height))); | ||
width: var(--k-size); | ||
height: var(--k-size); | ||
user-select: none; | ||
} | ||
[part~='knob']::after { | ||
content: ''; | ||
width: var(--vcf-slider-knob-size); | ||
@@ -92,8 +143,8 @@ height: var(--vcf-slider-knob-size); | ||
box-shadow: var(--lumo-box-shadow-s); | ||
user-select: none; | ||
background-color: var(--lumo-primary-color); | ||
background-color: var(--vcf-slider-knob-color); | ||
margin: auto; | ||
} | ||
[part~='knob'].alternate { | ||
background-color: var(--lumo-error-color); | ||
[part~='alt-knob']::after { | ||
background-color: var(--vcf-slider-knob-alt-color); | ||
} | ||
@@ -103,14 +154,89 @@ | ||
display: none; | ||
flex-flow: column; | ||
align-items: center; | ||
position: absolute; | ||
top: calc(-0.5 * var(--vs-k-size) + calc(0.5 * var(--vs-l-height)) - calc(2 * var(--lumo-space-m))); | ||
border-radius: var(--lumo-border-radius-s); | ||
bottom: calc(var(--k-size) * 0.5 + var(--lumo-space-xs)); | ||
border-radius: 2px; | ||
box-shadow: var(--lumo-box-shadow-xs); | ||
background-color: var(--lumo-base-color); | ||
padding: 0 var(--lumo-space-s); | ||
font-size: var(--lumo-font-size-s); | ||
padding: 2px var(--lumo-space-s); | ||
font-size: var(--vcf-slider-label-font-size); | ||
pointer-events: none; | ||
user-select: none; | ||
} | ||
[part~='label-triangle'] { | ||
position: relative; | ||
margin: 0; | ||
box-sizing: border-box; | ||
background: var(--lumo-base-color); | ||
} | ||
[part~='label-triangle']::after { | ||
content: ''; | ||
position: absolute; | ||
left: -4px; | ||
width: 0; | ||
height: 0; | ||
box-sizing: border-box; | ||
border: 3px solid transparent; | ||
border-color: transparent transparent var(--lumo-base-color) var(--lumo-base-color); | ||
transform-origin: 2px 1px; | ||
transform: rotate(-45deg); | ||
box-shadow: -2px 2px 2px 0 var(--lumo-shade-20pct); | ||
} | ||
:host([labels]) [part~='label'] { | ||
display: block; | ||
display: flex; | ||
} | ||
/* VERTICAL */ | ||
:host([labels][vertical]) { | ||
padding-top: 0px; | ||
padding-right: calc(var(--vcf-slider-label-width) + 4px + var(--lumo-space-xs)); | ||
} | ||
:host([vertical]) { | ||
display: inline-flex; | ||
width: max-content; | ||
margin: var(--lumo-space-s); | ||
height: var(--vcf-slider-vertical-height); | ||
} | ||
:host([vertical]) #line { | ||
width: var(--l-height); | ||
height: 100%; | ||
} | ||
:host([vertical]) [part~='knob'] { | ||
left: calc(-0.5 * var(--k-size) + calc(0.5 * var(--l-height))); | ||
} | ||
:host([vertical]) [part~='label'] { | ||
flex-flow: row; | ||
left: calc(var(--k-size) * 0.5 + var(--lumo-space-s)); | ||
bottom: unset; | ||
align-items: center; | ||
border-radius: var(--lumo-border-radius-s); | ||
} | ||
:host([vertical]) [part~='label-triangle']::after { | ||
content: unset; | ||
} | ||
:host([vertical]) [part~='label']::after { | ||
content: ''; | ||
position: absolute; | ||
left: 1px; | ||
width: 0px; | ||
height: 0px; | ||
box-sizing: border-box; | ||
border: 7px solid transparent; | ||
border-color: transparent transparent var(--lumo-base-color) var(--lumo-base-color); | ||
transform-origin: 4px -1px; | ||
transform: rotate(45deg); | ||
box-shadow: -2px 2px 2px 0 var(--lumo-shade-10pct); | ||
border-radius: 2px; | ||
} | ||
`; | ||
@@ -121,6 +247,6 @@ } | ||
return html` | ||
<div part="container"> | ||
<div part="line"> | ||
<div part="line-color"></div> | ||
<div part="knobs"></div> | ||
<div id="container" part="container"> | ||
<div id="line" part="line"> | ||
<div id="line-color" part="line-color"></div> | ||
<div id="knobs" part="knobs"></div> | ||
</div> | ||
@@ -132,57 +258,110 @@ </div> | ||
updated(props: PropertyValues) { | ||
props.forEach(async (_, prop) => { | ||
switch (prop) { | ||
case 'labels': { | ||
if (this.labels) this.style.paddingTop = 'var(--lumo-space-l)'; | ||
else this.style.paddingTop = '0'; | ||
break; | ||
} | ||
case 'ranges': { | ||
const { ranges } = this; | ||
if (typeof ranges === 'number' && ranges >= 0) this.knobs = 2 * ranges || 1; | ||
else this.ranges = 0; | ||
break; | ||
} | ||
case 'min': | ||
case 'max': | ||
case 'knobs': { | ||
if (this.knobs) { | ||
this.setKnobElements(); | ||
this.setInitialValue(); | ||
} | ||
break; | ||
} | ||
case 'step': { | ||
const { step } = this; | ||
if (step.toString().includes('.')) this.step = Math.round(step); | ||
if (step < 1) this.step = 1; | ||
break; | ||
} | ||
case 'value': { | ||
this.setLabelValues(); | ||
this.knobIndexes.forEach(i => { | ||
this.setAriaValues(i); | ||
this.setLabelPosition(i); | ||
this.setKnobPostion(i); | ||
this.setLineColors(); | ||
}); | ||
const { ranges, step } = this; | ||
// TODO Add events... | ||
this.dispatchEvent(new CustomEvent('change', { detail: { value: this.value } })); | ||
break; | ||
} | ||
} | ||
}); | ||
if (props.has('labels') && this.labels) { | ||
this.knobIndexes.forEach(i => this.setLabelPosition(i, this.values)); | ||
} | ||
if (props.has('ranges')) { | ||
if (typeof ranges === 'number' && ranges >= 0) this.knobs = 2 * ranges || 1; | ||
else this.ranges = 0; | ||
} | ||
if (props.has('ranges') || props.has('min') || props.has('max')) { | ||
this.setKnobElements(); | ||
this.setValue((this.value = this.initialValue)); | ||
} | ||
if (props.has('step')) { | ||
if (!Number.isSafeInteger(step)) this.step = Math.round(step); | ||
if (step < 1) this.step = 1; | ||
} | ||
if (props.has('value') || props.has('vertical')) { | ||
if (!this.isSorted()) this.sort(); | ||
else this.setValue(); | ||
} | ||
} | ||
private setLabelValues() { | ||
const { knobIndexes, values } = this; | ||
/** @private */ | ||
passive = true; | ||
/** @private */ | ||
handleEvent(e: Event) { | ||
switch (e.type) { | ||
case 'mousedown': | ||
case 'touchstart': | ||
if (!Slider.isValid(e) || this.isKnobClick(e)) return; | ||
this.knob?.focus(); | ||
this.startDrag(e); | ||
this.dragging = true; | ||
break; | ||
case 'mousemove': | ||
case 'touchmove': | ||
this.drag(e); | ||
break; | ||
case 'mouseup': | ||
case 'touchend': | ||
this.dragging = false; | ||
break; | ||
case 'keydown': | ||
this.keyMove(e, this.knobIndex); | ||
break; | ||
} | ||
} | ||
private static hasTouched = false; | ||
/** | ||
* Check if an event was triggered by touch. | ||
*/ | ||
private static isTouch(e: Event): e is TouchEvent { | ||
return 'touches' in e; | ||
} | ||
/** | ||
* Prevent mobile browsers from handling mouse events (conflicting with touch ones). | ||
* If we detected a touch interaction before, we prefer reacting to touch events only. | ||
*/ | ||
private static isValid(event: Event) { | ||
const { hasTouched, isTouch } = Slider; | ||
if (hasTouched && !isTouch(event)) return false; | ||
if (!hasTouched) Slider.hasTouched = isTouch(event); | ||
return true; | ||
} | ||
private get valueChangedEvent() { | ||
const detail = { | ||
index: this.knobIndex, | ||
value: this.values[this.knobIndex], | ||
values: this.values, | ||
}; | ||
const event = new CustomEvent(CustomEvents.valueChanged, { detail }); | ||
return event as ValueChangedEvent; | ||
} | ||
private static getKnobIndex(knob: HTMLElement) { | ||
const idMatch = /knob-(.)/.exec(knob.getAttribute('part') || ''); | ||
return idMatch ? Number(idMatch[1]) : 0; | ||
} | ||
private get knobIndex() { | ||
return this.knob ? Slider.getKnobIndex(this.knob) : 0; | ||
} | ||
private isKnobClick(e: Event) { | ||
return !Slider.hasTouched && (e as MouseEvent).button !== 0; | ||
} | ||
private setLabelValues(values = this.values) { | ||
const { knobIndexes } = this; | ||
knobIndexes.forEach(i => { | ||
const labelElement = this.labelElement(i) as HTMLElement; | ||
if (labelElement) labelElement.innerText = `${values[i]}`; | ||
const labelElementValue = labelElement.firstElementChild as HTMLSpanElement; | ||
if (labelElement) labelElementValue.innerText = `${values[i]}`; | ||
}); | ||
} | ||
private setAriaValues(i = 0) { | ||
const { values, knobs, min, max } = this; | ||
private setAriaValues(i = 0, values = this.values) { | ||
const { knobs, min, max } = this; | ||
const knob = this.knobElement(i) as HTMLElement; | ||
@@ -198,9 +377,11 @@ if (knob) { | ||
const { knobs, min, max, step } = this; | ||
const valueAttr = this.getAttribute('value'); | ||
const valueStep = (max - min) / (knobs - 1); | ||
const values: number[] = []; | ||
this.knobIndexes.map(i => { | ||
this.knobIndexes.forEach(i => { | ||
let init = Math.round(i === 0 ? min : i < knobs - 1 ? i * valueStep : max); | ||
init = init - (init % step); | ||
init -= init % step; | ||
values.push(init < min ? min : init > max ? max : init); | ||
}); | ||
if (valueAttr && !this.ranges) values[0] = Number(valueAttr); | ||
return values; | ||
@@ -210,39 +391,63 @@ } | ||
private get values() { | ||
const { value } = this; | ||
return (Array.isArray(value) ? value : [value || 0]) as number[]; | ||
let { value } = this; | ||
if (typeof value === 'string') value = JSON.parse(value[0] === '[' ? value : `[${value}]`); | ||
else value = (Array.isArray(value) ? value : [value || 0]) as number[]; | ||
return value as number[]; | ||
} | ||
private setInitialValue() { | ||
this.value = this.initialValue; | ||
this.knobIndexes.map(i => this.setKnobPostion(i)); | ||
this.setLineColors(); | ||
private setValue(values = this.values) { | ||
values = values.sort((a, b) => a - b); | ||
this.setLabelValues(values); | ||
this.knobIndexes.forEach(i => { | ||
this.setAriaValues(i, values); | ||
this.setLabelPosition(i, values); | ||
this.setKnobPostion(i, values); | ||
this.setLineColors(values); | ||
}); | ||
this.dispatchEvent(this.valueChangedEvent); | ||
} | ||
private setKnobPostion(i = 0) { | ||
const { min, max, values } = this; | ||
const lineWidth = this.lineBounds!.width; | ||
private setKnobPostion(i = 0, values = this.initialValue) { | ||
const { min, max, lineBounds } = this; | ||
const knob = this.knobElement(i) as HTMLElement; | ||
if (knob) { | ||
const knobWidth = this.getBounds(knob).width; | ||
const position = ((values[i] - min) / (max - min)) * lineWidth - knobWidth / 2; | ||
knob.style.left = `${position}px`; | ||
const knobBounds = this.getBounds(knob); | ||
if (this.vertical) { | ||
const position = ((values[i] - min) / (max - min)) * lineBounds.height - knobBounds.height / 2; | ||
knob.style.top = 'unset'; | ||
knob.style.bottom = `${position}px`; | ||
knob.style.removeProperty('left'); | ||
} else { | ||
const position = ((values[i] - min) / (max - min)) * lineBounds.width - knobBounds.width / 2; | ||
knob.style.left = `${position}px`; | ||
knob.style.removeProperty('top'); | ||
knob.style.removeProperty('bottom'); | ||
} | ||
} | ||
} | ||
private setLabelPosition(i = 0) { | ||
const { min, max, values } = this; | ||
const lineWidth = this.lineBounds!.width; | ||
private setLabelPosition(i = 0, values = this.values) { | ||
const { min, max, lineBounds } = this; | ||
const label = this.labelElement(i) as HTMLElement; | ||
if (label) { | ||
const labelWidth = this.getBounds(label).width; | ||
const position = ((values[i] - min) / (max - min)) * lineWidth - labelWidth / 2; | ||
label.style.left = `${position}px`; | ||
const labelBounds = this.getBounds(label); | ||
if (this.vertical) { | ||
const position = ((values[i] - min) / (max - min)) * lineBounds.height - labelBounds.height / 2; | ||
label.style.bottom = `${position}px`; | ||
label.style.removeProperty('left'); | ||
} else { | ||
const position = ((values[i] - min) / (max - min)) * lineBounds.width - labelBounds.width / 2; | ||
label.style.left = `${position}px`; | ||
label.style.removeProperty('bottom'); | ||
} | ||
this.style.setProperty('--vcf-slider-label-width', `${labelBounds.width}px`); | ||
} | ||
} | ||
private setLineColors() { | ||
const { knobs, min, max, values } = this; | ||
private setLineColors(values = this.values) { | ||
const { knobs, min, max, lineColorElement } = this; | ||
const length = max - min; | ||
const lineColor = getComputedStyle(this).getPropertyValue('--vcf-slider-line-color').trim(); | ||
const altLineColor = getComputedStyle(this).getPropertyValue('--vcf-slider-line-alternate-color').trim(); | ||
const altLineColor = getComputedStyle(this).getPropertyValue('--vcf-slider-line-alt-color').trim(); | ||
const direction = this.vertical ? 'top' : 'right'; | ||
let colors = ''; | ||
@@ -259,38 +464,40 @@ let prevStop = ''; | ||
colors += `transparent ${prevStop} 100%`; | ||
this.lineColor!.style.background = `linear-gradient(to right, ${colors})`; | ||
if (lineColorElement) lineColorElement.style.background = `linear-gradient(to ${direction}, ${colors})`; | ||
} | ||
private getKnobIndex(knob: HTMLElement) { | ||
const idMatch = /knob-(.)/.exec(knob.getAttribute('part') || ''); | ||
return idMatch ? Number(idMatch[1]) : 0; | ||
private set dragging(state: boolean) { | ||
const toggleEvent = state ? document.addEventListener : document.removeEventListener; | ||
toggleEvent(Slider.hasTouched ? 'touchmove' : 'mousemove', this); | ||
toggleEvent(Slider.hasTouched ? 'touchend' : 'mouseup', this); | ||
} | ||
private startDrag = (e: PointerEvent) => { | ||
private getPointerXY(e: Event) { | ||
let pointerPos: number | null = null; | ||
if (e instanceof MouseEvent) pointerPos = (e as MouseEvent)[this.pageXY]; | ||
else if (e instanceof TouchEvent) pointerPos = (e as TouchEvent).touches[0][this.pageXY]; | ||
return pointerPos; | ||
} | ||
private startDrag = (e: Event) => { | ||
const { knobsContainer, xy } = this; | ||
this.knob = e.target as HTMLElement; | ||
const { knob, label, knobsContainer } = this; | ||
const button = (e as MouseEvent).button; | ||
const touches = (e as TouchEvent).touches; | ||
this.originalPointerXY = this.getPointerXY(e); | ||
this.originalKnobOffsetXY = this.getBounds(this.knob)[xy] - this.lineBounds[xy]; | ||
this.originalPointerX = (e as MouseEvent).pageX; | ||
this.originalKnobOffsetX = this.getBounds(knob).x - this.lineBounds!.x; | ||
if (button === 0 || touches) { | ||
window.addEventListener('mouseup', this.endDrag); | ||
window.addEventListener('touchend', this.endDrag); | ||
window.addEventListener('mousemove', this.drag); | ||
window.addEventListener('touchmove', this.drag); | ||
} | ||
// Move current knob and label to top | ||
knobsContainer?.appendChild(knob); | ||
knobsContainer?.appendChild(label); | ||
knobsContainer?.appendChild(this.knob); | ||
knobsContainer?.appendChild(this.label); | ||
}; | ||
private drag = (e: PointerEvent) => { | ||
const { knob, knobs, originalKnobOffsetX, originalPointerX, line, lineBounds } = this; | ||
if (knob) { | ||
const i = this.getKnobIndex(knob); | ||
private drag = (e: Event) => { | ||
const { knobIndex: i, vertical, knob, knobs, originalKnobOffsetXY, originalPointerXY, xy, line, lineBounds } = this; | ||
if (knob && line) { | ||
const knobBounds = this.getBounds(knob); | ||
let startX = -knobBounds.width / 2; | ||
let endX = lineBounds!.width - knobBounds.width / 2; | ||
const knobSize = vertical ? knobBounds.height : knobBounds.width; | ||
const lineSize = vertical ? lineBounds.height : lineBounds.width; | ||
const lineStart = -knobSize / 2; | ||
const lineEnd = lineSize - knobSize / 2; | ||
const part = `knob-${i}`; | ||
let start = vertical ? lineEnd : lineStart; | ||
let end = vertical ? lineStart : lineEnd; | ||
@@ -301,4 +508,4 @@ // Set knob limits | ||
if (knobs > 1) { | ||
const toKnob = line!.querySelector('[part~="knob-1"]') as HTMLElement; | ||
endX = this.getBounds(toKnob).x - lineBounds!.x; | ||
const toKnob = line.querySelector('[part~="knob-1"]') as HTMLElement; | ||
end = this.getBounds(toKnob)[xy] - lineBounds[xy]; | ||
} | ||
@@ -309,4 +516,4 @@ break; | ||
if (knobs > 1) { | ||
const fromKnob = line!.querySelector(`[part~="knob-${knobs - 2}"]`) as HTMLElement; | ||
startX = this.getBounds(fromKnob).x - lineBounds!.x; | ||
const fromKnob = line.querySelector(`[part~="knob-${knobs - 2}"]`) as HTMLElement; | ||
start = this.getBounds(fromKnob)[xy] - lineBounds[xy]; | ||
} | ||
@@ -316,6 +523,6 @@ break; | ||
default: { | ||
const fromKnob = line!.querySelector(`[part~="knob-${i - 1}"]`) as HTMLElement; | ||
const toKnob = line!.querySelector(`[part~="knob-${i + 1}"]`) as HTMLElement; | ||
startX = this.getBounds(fromKnob).x - lineBounds!.x; | ||
endX = this.getBounds(toKnob).x - lineBounds!.x; | ||
const fromKnob = line.querySelector(`[part~="knob-${i - 1}"]`) as HTMLElement; | ||
const toKnob = line.querySelector(`[part~="knob-${i + 1}"]`) as HTMLElement; | ||
start = this.getBounds(fromKnob)[xy] - lineBounds[xy]; | ||
end = this.getBounds(toKnob)[xy] - lineBounds[xy]; | ||
} | ||
@@ -325,24 +532,28 @@ } | ||
// Calculate knob position | ||
let newPositionX = originalKnobOffsetX + ((e as MouseEvent).pageX - originalPointerX); | ||
const startLimit = newPositionX <= startX; | ||
const endLimit = newPositionX >= endX; | ||
newPositionX = startLimit ? startX : endLimit ? endX : newPositionX; | ||
const pageXY = this.getPointerXY(e); | ||
if (pageXY && originalPointerXY) { | ||
let newPositionXY = originalKnobOffsetXY + (pageXY - originalPointerXY); | ||
let startLimit = vertical ? newPositionXY >= start : newPositionXY <= start; | ||
let endLimit = vertical ? newPositionXY <= end : newPositionXY >= end; | ||
newPositionXY = startLimit ? start : endLimit ? end : newPositionXY; | ||
// Calculate new value | ||
const { min, max, step, values } = this; | ||
const length = max - min; | ||
const pct = (newPositionX + knobBounds.width / 2) / lineBounds!.width; | ||
const value = Math.round(pct * length + min); | ||
// Calculate new value | ||
const { min, max, step, values } = this; | ||
const length = max - min; | ||
const pct = (newPositionXY + knobSize / 2) / lineSize; | ||
let value = Math.round(pct * length + min); | ||
if (vertical) value = max - value; | ||
// Step | ||
if (value === min || (value > min && Math.abs(value) % step === 0)) { | ||
// Set new value & knob position | ||
if (values[i] !== value) { | ||
values[i] = value; | ||
this.value = this.knobs === 1 ? value : [...values]; | ||
this.setKnobPostion(i); | ||
// Step | ||
if (value === min || (value > min && Math.abs(value) % step === 0)) { | ||
// Set new value & knob position | ||
if (values[i] !== value) { | ||
values[i] = value; | ||
this.value = this.knobs === 1 ? value : [...values]; | ||
this.setKnobPostion(i); | ||
} | ||
// Change line colors | ||
this.setLineColors(); | ||
} | ||
// Change line colors | ||
this.setLineColors(); | ||
} | ||
@@ -352,12 +563,4 @@ } | ||
private endDrag = () => { | ||
window.removeEventListener('mouseup', this.endDrag); | ||
window.removeEventListener('touchend', this.endDrag); | ||
window.removeEventListener('mousemove', this.drag); | ||
window.removeEventListener('touchmove', this.drag); | ||
}; | ||
private get label() { | ||
const i = this.knob ? this.getKnobIndex(this.knob) : 0; | ||
return this.labelElement(i) as HTMLElement; | ||
return this.labelElement(this.knobIndex) as HTMLElement; | ||
} | ||
@@ -378,4 +581,4 @@ | ||
private setKnobElements() { | ||
const { knobs, knobIndexes, knobsContainer } = this; | ||
if (knobs && knobsContainer) { | ||
const { knobIndexes, knobsContainer } = this; | ||
if (knobsContainer) { | ||
knobsContainer.innerHTML = ''; | ||
@@ -389,17 +592,28 @@ knobIndexes.map(i => { | ||
private createKnobElement(knobIndex: number): HTMLDivElement { | ||
const knobElement = document.createElement('div'); | ||
knobElement.tabIndex = knobIndex + 1; | ||
knobElement.setAttribute('role', 'slider'); | ||
knobElement.setAttribute('part', `knob knob-${knobIndex}`); | ||
knobElement.addEventListener('mousedown', this.startDrag); | ||
if (knobIndex % 2) knobElement.classList.add('alternate'); | ||
knobElement.addEventListener('keydown', event => this.handleKnobKeyDownEvent(event, knobIndex)); | ||
return knobElement; | ||
private createKnobElement(knobIndex: number) { | ||
const isAltKnob = knobIndex % 2 ? 'alt-knob' : ''; | ||
const handleEvent = this as { handleEvent: EventListener }; | ||
return this.createElement( | ||
html` | ||
<div | ||
role="slider" | ||
part="knob ${isAltKnob} knob-${knobIndex}" | ||
tabindex="${knobIndex + 1}" | ||
@mousedown="${handleEvent}" | ||
@touchstart="${handleEvent}" | ||
@keydown="${handleEvent}" | ||
></div> | ||
` | ||
); | ||
} | ||
private createKnobLabelElement(knobIndex: number): HTMLDivElement { | ||
const labelElement = document.createElement('div'); | ||
labelElement.setAttribute('part', `label label-${knobIndex}`); | ||
return labelElement; | ||
private createKnobLabelElement(knobIndex: number) { | ||
return this.createElement( | ||
html` | ||
<div part="label label-${knobIndex}"> | ||
<span part="label-value label-value-${knobIndex}"></span> | ||
<div part="label-triangle label-triangle-${knobIndex}"></div> | ||
</div> | ||
` | ||
); | ||
} | ||
@@ -413,3 +627,3 @@ | ||
if (neighborPrecisionOffset) neighboringValue += step - neighborPrecisionOffset; | ||
return neighboringValue ? neighboringValue : min; | ||
return neighboringValue || min; | ||
} | ||
@@ -423,3 +637,3 @@ | ||
if (neighborPrecisionOffset) neighboringValue -= neighborPrecisionOffset; | ||
return neighboringValue ? neighboringValue : max; | ||
return neighboringValue || max; | ||
} | ||
@@ -456,3 +670,3 @@ | ||
// Use the smallest number between max value, requested value, and the neighboring value. | ||
other: Math.max(min, values[knobIndex] - step, this.getPrevNeighborValue(knobIndex)) | ||
other: Math.max(min, values[knobIndex] - step, this.getPrevNeighborValue(knobIndex)), | ||
}); | ||
@@ -468,3 +682,3 @@ } | ||
// Use the biggest number between min value and the neighboring value. | ||
other: Math.max(min, this.getPrevNeighborValue(knobIndex)) | ||
other: Math.max(min, this.getPrevNeighborValue(knobIndex)), | ||
}); | ||
@@ -482,3 +696,3 @@ } | ||
// Use the smallest number between max value, requested value, and the neighboring value. | ||
other: Math.min(max, values[knobIndex] + step, this.getNextNeighborValue(knobIndex)) | ||
other: Math.min(max, values[knobIndex] + step, this.getNextNeighborValue(knobIndex)), | ||
}); | ||
@@ -494,9 +708,9 @@ } | ||
// Use the smallest number between max value and the neighboring value. | ||
other: Math.min(this.max, this.getNextNeighborValue(knobIndex)) | ||
other: Math.min(this.max, this.getNextNeighborValue(knobIndex)), | ||
}); | ||
} | ||
private handleKnobKeyDownEvent(event: KeyboardEvent, knobIndex: number) { | ||
private keyMove(event: Event, knobIndex: number) { | ||
let flag = false; | ||
const key = event.key || event.keyCode; | ||
const key = (event as KeyboardEvent).key || (event as KeyboardEvent).keyCode; | ||
switch (key) { | ||
@@ -510,3 +724,2 @@ case 'ArrowLeft': | ||
break; | ||
case 'ArrowRight': | ||
@@ -519,3 +732,2 @@ case 39: | ||
break; | ||
case 'Home': | ||
@@ -526,3 +738,2 @@ case 36: | ||
break; | ||
case 'End': | ||
@@ -533,3 +744,2 @@ case 35: | ||
break; | ||
default: | ||
@@ -549,4 +759,16 @@ break; | ||
private get lineBounds() { | ||
return this.getBounds(this.line!); | ||
return this.getBounds(this.line as HTMLElement); | ||
} | ||
private createElement(template: TemplateResult) { | ||
return (render(template, document.createElement('x')).parentNode as HTMLElement).firstElementChild as HTMLElement; | ||
} | ||
private sort() { | ||
this.value = this.values.sort((a, b) => a - b); | ||
} | ||
private isSorted(values = this.values) { | ||
return values.every((_, i, a) => !a[i - 1] || a[i] >= a[i - 1]); | ||
} | ||
} | ||
@@ -556,3 +778,3 @@ | ||
interface HTMLElementTagNameMap { | ||
'vcf-slider': VcfSlider; | ||
'vcf-slider': Slider; | ||
} | ||
@@ -559,0 +781,0 @@ } |
@@ -1,1 +0,3 @@ | ||
export { VcfSlider } from './src/vcf-slider.js'; | ||
import './src/vcf-slider.js'; | ||
export { Slider as VcfSlider } from './src/vcf-slider.js'; |
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
198670
19
2191
Yes
2
25
1
+ Added@open-wc/dedupe-mixin@1.4.0(transitive)
+ Added@vaadin/vaadin-themable-mixin@23.5.9(transitive)
Updatedlit@^2.4.1