Created
December 31, 2013 01:51
-
-
Save jarek-foksa/8191160 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @info | |
| // Slider widget heavily inspired by sliders from Blender and Expression Design. | |
| import {Stepper} from './Stepper'; | |
| import {HTML} from '../utils/dom'; | |
| import {getKeysForEvent} from '../utils/event'; | |
| import {normalize, round, log, exp} from '../utils/math'; | |
| import {mergeOptions} from '../utils/object'; | |
| import {makeSelectionFromNodeContent, getSelection} from '../utils/text'; | |
| import {isFinite, isNumeric} from '../utils/type'; | |
| let SliderOptions = { | |
| id: 'sample-slider', | |
| tooltip: 'This is sample tooltip', | |
| style: 'normal', // normal/toolbar | |
| width: 150, | |
| height: 28, | |
| value: 70, | |
| prefix: 'Value: ', | |
| postfix: '%', | |
| enabled: true, | |
| scale: 'lin', // lin/log | |
| minValue: 0, // must be finite if log scale is used | |
| maxValue: 100, // must be finitie if log scale is used | |
| valueIncrement: 1, | |
| maxPrecission: 2, | |
| optimalPrecission: 2 // precission used when value is changed with stepper or mouse drag (lin scale only) | |
| }; | |
| // @events | |
| // userWillStartChangingValue | |
| // userChangedValue (value) | |
| // userFinishedChangingValue (didChange) | |
| // clicked | |
| export class Slider { | |
| constructor(options = SliderOptions) { | |
| options = mergeOptions(options, SliderOptions); | |
| this.scale = options.scale; | |
| this.minValue = options.minValue; | |
| this.maxValue = options.maxValue; | |
| this.valueIncrement = options.valueIncrement; | |
| this.maxPrecission = options.maxPrecission; | |
| this.optimalPrecission = options.optimalPrecission; | |
| this.focused = false; | |
| this._cachedValue = null; | |
| if (isFinite(this.minValue) && isFinite(this.maxValue)) { | |
| this._showBar = true; | |
| } | |
| else { | |
| this._showBar = false; | |
| } | |
| this.element = HTML`<div class="slider x-slider"></div>`; | |
| this.element.data.set('focused', false); | |
| this.element.owner = this; | |
| this._$bar = HTML`<div class="bar"></div>`; | |
| this._$bar.appendTo(this.element); | |
| this._$text = HTML`<div class="text" spellcheck="false"></div>`; | |
| this._$text.appendTo(this.element); | |
| this._$prefix = HTML`<span class="prefix"></span>`; | |
| this._$prefix.appendTo(this._$text); | |
| this._$value = HTML`<span class="value"></span>`; | |
| this._$value.appendTo(this._$text); | |
| this._$postfix = HTML`<span class="postfix"></span>`; | |
| this._$postfix.appendTo(this._$text); | |
| this._$input = HTML`<input></input>`; | |
| this._$input.tabIndex = -1; | |
| this._$input.appendTo(this.element); | |
| this._stepper = new Stepper(); | |
| this._stepper.element.appendTo(this.element); | |
| this.setID(options.id); | |
| this.setTooltip(options.tooltip); | |
| this.setStyle(options.style); | |
| this.setWidth(options.width); | |
| this.setHeight(options.height); | |
| this.setValue(options.value); | |
| this.setPrefix(options.prefix); | |
| this.setPostfix(options.postfix); | |
| if (options.enabled) { | |
| this.enable(); | |
| } | |
| else { | |
| this.disable(); | |
| } | |
| this._hideStepper(); | |
| } | |
| free() { | |
| this.disable(); | |
| this.element.remove(); | |
| this._stepper.free(); | |
| } | |
| enable() { | |
| if (this.enabled === true) { | |
| return; | |
| } | |
| this.enabled = true; | |
| this.element.data.set('enabled', true); | |
| this.element.addEventListener('mousedown', this._mouseDownCB = (event) => { | |
| if (!this.focused && !this._stepper.element.contains(event.target)) { | |
| this._onMouseDown(event); | |
| } | |
| }); | |
| this.element.addEventListener('mouseenter', this._mouseEnterCB = () => { | |
| if (!this.focused && !pointerManager.isDragging) { | |
| this._showStepper(); | |
| } | |
| }); | |
| this.element.addEventListener('mouseleave', this._mouseLeaveCB = () => { | |
| if (!this.focused && !pointerManager.isDragging) { | |
| this._hideStepper(); | |
| } | |
| }); | |
| this._stepper.listen('pressedArrow', this._stepperPressedCB = (arrow) => { | |
| this._onStepperArrowPressed(arrow); | |
| }); | |
| } | |
| disable(reset = true) { | |
| if (this.enabled === false) { | |
| return; | |
| } | |
| this.enabled = false; | |
| this.element.data.set('enabled', false); | |
| if (reset) { | |
| if (isFinite(this.minValue)) { | |
| this.setValue(this.minValue); | |
| } | |
| else { | |
| this.setValue(0); | |
| } | |
| } | |
| this.element.removeEventListener('mousedown', this._mouseDownCB); | |
| this.element.removeEventListener('mouseenter', this._mouseEnterCB); | |
| this.element.removeEventListener('mouseleave', this._mouseLeaveCB); | |
| this._stepper.unlisten('pressedArrow', this._stepperPressedCB); | |
| } | |
| focus() { | |
| if (this.focused) { | |
| return; | |
| } | |
| this.element.data.set('focused', true); | |
| this.focused = true; | |
| this._$input.value = this._getValueStringForDisplay(this.value); | |
| this._$input.focus(); | |
| this._$input.select(); | |
| this._hideStepper(); | |
| this._initialValue = this.value; | |
| this._cachedValue = this.value; | |
| this.trigger('userWillStartChangingValue', this.value); | |
| this.trigger('clicked'); | |
| window.addEventListener('blur', this._focusCB0 = (event) => { | |
| this.blur(); | |
| }); | |
| document.addEventListener('mousedown', this._focusCB1 = (event) => { | |
| if (!this.element.contains(event.target)) { | |
| this.blur(); | |
| } | |
| }, true); | |
| this._$input.addEventListener('input', this._focusCB3 = (event) => { | |
| this._onInput(); | |
| }); | |
| commandsManager.register(this._selectAllCommand = { | |
| id: 'selectAll', | |
| shortcut: ['Ctrl', 'A'], | |
| getLabel: () => 'Select All', | |
| getEnabled: () => true, | |
| run: () => this._$input.select() | |
| }); | |
| commandsManager.register(this._copyCommand = { | |
| id: 'copy', | |
| shortcut: ['Ctrl', 'C'], | |
| getLabel: () => 'Copy', | |
| getEnabled: () => (this._$input.selectionStart !== this._$input.selectionEnd), | |
| run: () => { | |
| let start = this._$input.selectionStart; | |
| let end = this._$input.selectionEnd; | |
| let string = this._$input.value.substring(start, end); | |
| backend.postMessage('setClipboardData', [string, null]); | |
| } | |
| }); | |
| commandsManager.register(this._cutCommand = { | |
| id: 'cut', | |
| shortcut: ['Ctrl', 'X'], | |
| getLabel: () => 'Cut', | |
| getEnabled: () => (this._$input.selectionStart !== this._$input.selectionEnd), | |
| run: () => this._cutValue() | |
| }); | |
| commandsManager.register(this._pasteCommand = { | |
| id: 'paste', | |
| shortcut: ['Ctrl', 'V'], | |
| getLabel: () => 'Paste', | |
| getEnabled: () => true, | |
| run: () => this._pasteValue() | |
| }); | |
| commandsManager.register(this._blurSliderCommand = { | |
| id: 'blurSlider', | |
| shortcut: ['Enter'], | |
| hidden: true, | |
| run: () => this.blur() | |
| }); | |
| commandsManager.register(this._incrementSliderValueSmallCommand = { | |
| id: 'incrementSliderValueSmall', | |
| shortcut: ['Up'], | |
| hidden: true, | |
| run: () => { | |
| this.setValue(this.value + this.valueIncrement); | |
| this.trigger('userChangedValue', this.value); | |
| this._$input.select(); | |
| } | |
| }); | |
| commandsManager.register(this._decrementSliderValueSmallCommand = { | |
| id: 'decrementSliderValueSmall', | |
| shortcut: ['Down'], | |
| hidden: true, | |
| run: () => { | |
| this.setValue(this.value - this.valueIncrement); | |
| this.trigger('userChangedValue', this.value); | |
| this._$input.select(); | |
| } | |
| }); | |
| commandsManager.register(this._incrementSliderValueBigCommand = { | |
| id: 'incrementSliderValueBig', | |
| shortcut: ['Shift', 'Up'], | |
| hidden: true, | |
| run: () => { | |
| this.setValue(this.value + (this.valueIncrement * 10)); | |
| this.trigger('userChangedValue', this.value); | |
| this._$input.select(); | |
| } | |
| }); | |
| commandsManager.register(this._decrementSliderValueBigCommand = { | |
| id: 'decrementSliderValueBig', | |
| shortcut: ['Shift', 'Down'], | |
| hidden: true, | |
| run: () => { | |
| this.setValue(this.value - (this.valueIncrement * 10)); | |
| this.trigger('userChangedValue', this.value); | |
| this._$input.select(); | |
| } | |
| }); | |
| // Filter out commands that could disturb user while typing text | |
| commandsManager.addShortcutFilter(this._focusFilter = (shortcut) => { | |
| for (let allowedShortcut of [['Up'], ['Down'], ['Shift', 'Up'], ['Shift', 'Down'], ['Enter']]) { | |
| if (shortcut.compare(allowedShortcut)) { | |
| return true; | |
| } | |
| } | |
| if (shortcut.contains('Ctrl') || shortcut.contains('Alt')) { | |
| return true; | |
| } | |
| else { | |
| return false; | |
| } | |
| }); | |
| } | |
| blur() { | |
| if (this.focused === false) { | |
| return; | |
| } | |
| this.focused = false; | |
| this.element.data.set('focused', false); | |
| this.element.data.set('valid', true); | |
| this._$value.innerHTML = this._getValueStringForDisplay(this.value); | |
| this._$input.blur(); | |
| if (this._isHovered()) { | |
| this._showStepper(); | |
| } | |
| let didChange = (this.value !== this._cachedValue); | |
| this.trigger('userFinishedChangingValue', didChange); | |
| window.removeEventListener('blur', this._focusCB0); | |
| document.removeEventListener('mousedown', this._focusCB1); | |
| this._$input.removeEventListener('input', this._focusCB3); | |
| commandsManager.unregister(this._selectAllCommand); | |
| commandsManager.unregister(this._copyCommand); | |
| commandsManager.unregister(this._cutCommand); | |
| commandsManager.unregister(this._pasteCommand); | |
| commandsManager.unregister(this._blurSliderCommand); | |
| commandsManager.unregister(this._incrementSliderValueSmallCommand); | |
| commandsManager.unregister(this._decrementSliderValueSmallCommand); | |
| commandsManager.unregister(this._incrementSliderValueBigCommand); | |
| commandsManager.unregister(this._decrementSliderValueBigCommand); | |
| commandsManager.removeShortcutFilter(this._focusFilter); | |
| } | |
| setValue(value, average = false) { | |
| this.value = this._normalizeValue(value, 'max'); | |
| this.element.data.set('valid', true); | |
| this.element.data.set('average', average); | |
| this.trigger('changedValue', value); | |
| this._$value.innerHTML = this._getValueStringForDisplay(this.value); | |
| if (this.focused) { | |
| this._$input.value = this.value; | |
| } | |
| if (this._showBar) { | |
| this._redrawBar(); | |
| } | |
| } | |
| setID(id) { | |
| this.id = id; | |
| this.element.data.set('id', this.id); | |
| } | |
| setTooltip(tooltip) { | |
| this.tooltip = tooltip; | |
| this.element.setAttribute('title', this.tooltip); | |
| } | |
| setStyle(style) { | |
| this.style = style; | |
| this.element.data.set('style', style); | |
| } | |
| setWidth(width) { | |
| this.width = width; | |
| this.element.style.width = width + 'px'; | |
| if (this._showBar) { | |
| this._redrawBar(); | |
| } | |
| } | |
| setHeight(height) { | |
| this.height = height; | |
| this.element.style.height = height + 'px'; | |
| } | |
| setPrefix(prefix) { | |
| this.prefix = prefix; | |
| this._$prefix.textContent = prefix; | |
| } | |
| setPostfix(postfix) { | |
| this.postfix = postfix; | |
| this._$postfix.textContent = postfix; | |
| } | |
| _showStepper() { | |
| this._stepper.element.style.display = null; | |
| } | |
| _hideStepper() { | |
| this._stepper.element.style.display = 'none'; | |
| } | |
| _isHovered() { | |
| let $hovered = pointerManager.getHoveredElement(); | |
| let isHovered = (this.element === $hovered || this.element.contains($hovered)); | |
| return isHovered; | |
| } | |
| _redrawBar() { | |
| let barWidth; | |
| if (this.scale === 'lin') { | |
| let minBarWidth = (this.minValue / this.maxValue) * this.width; | |
| let maxBarWidth = this.width; | |
| barWidth = (this.value / this.maxValue) * this.width; | |
| barWidth = normalize(barWidth, minBarWidth, maxBarWidth, 2); | |
| } | |
| else if (this.scale === 'log') { | |
| let minBarWidth = (this.minValue / this.maxValue) * this.width; | |
| let maxBarWidth = this.width; | |
| let minValueLog = log(this.minValue); | |
| let maxValueLog = log(this.maxValue); | |
| let scale = (maxValueLog - minValueLog) / (maxBarWidth - minBarWidth); | |
| barWidth = ((log(this.value) - minValueLog) / scale) + minBarWidth; | |
| } | |
| this._$bar.style.width = `${barWidth}px`; | |
| } | |
| _onInput() { | |
| if (isNumeric(this._$input.value)) { | |
| this.value = this._normalizeValue(parseFloat(this._$input.value), 'max'); | |
| this.element.data.set('valid', true); | |
| this.element.data.set('average', false); | |
| this.trigger('changedValue', this.value); | |
| this.trigger('userChangedValue', this.value); | |
| } | |
| else { | |
| this.element.data.set('valid', false); | |
| } | |
| } | |
| _onMouseDown(mousedownEvent) { | |
| let valueInitial = this.value; | |
| let barWidthInitial = parseFloat(this._$bar.style.width); | |
| let resizeCursor = null; | |
| let isDragging = false; | |
| let cachedX = null; | |
| let dragValueIncrement; | |
| let mousemoveCB; | |
| let mouseupCB; | |
| if (this._showBar) { | |
| dragValueIncrement = (this.maxValue - this.minValue) / this.width; | |
| } | |
| else { | |
| dragValueIncrement = this.valueIncrement; | |
| } | |
| this._hideStepper(); | |
| document.addEventListener('mousemove', mousemoveCB = (mousemoveEvent) => { | |
| if (mousemoveEvent.clientX === cachedX) { | |
| return; | |
| } | |
| cachedX = mousemoveEvent.clientX; | |
| let dragOffset = mousemoveEvent.clientX - mousedownEvent.clientX; | |
| if (isDragging === false) { | |
| if (dragOffset >= -1 && dragOffset <= 1) { | |
| return; | |
| } | |
| else { | |
| this._cachedValue = this.value; | |
| this.trigger('userWillStartChangingValue', this.value); | |
| isDragging = true; | |
| resizeCursor = pointerManager.registerCursor('col-resize', 'high'); | |
| } | |
| } | |
| let value; | |
| if (this.scale === 'lin') { | |
| value = valueInitial + (dragOffset * dragValueIncrement); | |
| value = this._normalizeValue(value, 'optimal'); | |
| } | |
| else if (this.scale === 'log') { | |
| value = this._getLogValueForBarWidth(barWidthInitial + dragOffset); | |
| value = this._normalizeLogValue(value); | |
| } | |
| this.setValue(value); | |
| this.trigger('userChangedValue', this.value); | |
| }); | |
| document.addEventListener('mouseup', mouseupCB = (mouseupEvent) => { | |
| document.removeEventListener('mousemove', mousemoveCB); | |
| document.removeEventListener('mouseup', mouseupCB); | |
| pointerManager.unregisterCursor(resizeCursor); | |
| if (this._isHovered()) { | |
| this._showStepper(); | |
| } | |
| else { | |
| this._hideStepper(); | |
| } | |
| if (isDragging) { | |
| let didChange = (this.value !== this._cachedValue); | |
| this.trigger('userFinishedChangingValue', didChange); | |
| } | |
| else if (!this.focused) { | |
| this.focus(); | |
| } | |
| }); | |
| } | |
| _onStepperArrowPressed(arrow) { | |
| this._cachedValue = this.value; | |
| this.trigger('userWillStartChangingValue', this.value); | |
| let increment = () => { | |
| let value; | |
| if (arrow === 'up') { | |
| value = this.value + (1 * this.valueIncrement); | |
| } | |
| else { | |
| value = this.value - (1 * this.valueIncrement); | |
| } | |
| this.setValue(this._normalizeValue(value), 'optimal'); | |
| this.trigger('userChangedValue', this.value); | |
| }; | |
| let timer = 0; | |
| let interval = setInterval( () => { | |
| if (timer > 400) { | |
| increment(); | |
| } | |
| timer += 100; | |
| }, 100); | |
| increment(); | |
| let cb; | |
| this._stepper.listen('releasedArrow', cb = (arrow) => { | |
| this._stepper.unlisten('releasedArrow', cb); | |
| let didChange = (this.value !== this._cachedValue); | |
| this.trigger('userFinishedChangingValue', didChange); | |
| clearInterval(interval); | |
| }); | |
| } | |
| _cutValue() { | |
| let cursorPosition = this._$input.selectionStart; | |
| let cutString = this._$input.value.substring(this._$input.selectionStart, this._$input.selectionEnd); | |
| this._$input.value = this._$input.value.substring(0, this._$input.selectionStart) + | |
| this._$input.value.substring(this._$input.selectionEnd, this._$input.value.length); | |
| this._$input.setSelectionRange(cursorPosition, cursorPosition); | |
| this._onInput(); | |
| backend.postMessage('setClipboardData', [cutString, null]); | |
| } | |
| _pasteValue() { | |
| backend.postMessage('getClipboardData', (data) => { | |
| let text = data[0]; | |
| if (text && text.length > 0) { | |
| let cursorPosition = this._$input.selectionStart + text.length; | |
| this._$input.value = this._$input.value.substring(0, this._$input.selectionStart) + text + | |
| this._$input.value.substring(this._$input.selectionEnd, this._$input.value.length); | |
| this._$input.setSelectionRange(cursorPosition, cursorPosition); | |
| this._onInput(); | |
| } | |
| }); | |
| } | |
| // Convert specified value to string and get rid of any trailing zeros if the value has greater | |
| // precission than this.optimalPrecission | |
| _getValueStringForDisplay(value) { | |
| let value; | |
| if (this.scale === 'lin') { | |
| value = value.toFixed(this.maxPrecission).split(''); | |
| let dotIndex = value.indexOf('.'); | |
| if (dotIndex !== -1) { | |
| for (let i = value.length-1; i < dotIndex+this.optimalPrecission; i += 1) { | |
| if (value[i] === '0') { | |
| value.pop(); | |
| } | |
| else { | |
| break; | |
| } | |
| } | |
| if (value[value.length-1] === '.') { | |
| value.pop(); | |
| } | |
| } | |
| value = value.join(''); | |
| } | |
| else if (this.scale === 'log') { | |
| if (value >= 10) { | |
| value = value.toFixed(0); | |
| } | |
| else { | |
| value = value.toFixed(2); | |
| } | |
| } | |
| return value; | |
| } | |
| _normalizeValue(value, precission = 'max') { | |
| let precission; | |
| if (precission === 'max') { | |
| precission = this.maxPrecission; | |
| } | |
| else if (precission === 'optimal') { | |
| precission = this.optimalPrecission; | |
| } | |
| let value = normalize(value, this.minValue, this.maxValue, precission); | |
| return value; | |
| } | |
| _normalizeLogValue(value) { | |
| if (value >= 10) { | |
| value = round(value, 0); | |
| } | |
| else { | |
| value = round(value, 2); | |
| } | |
| return value; | |
| } | |
| // this.doc http://stackoverflow.com/questions/846221/logarithmic-slider | |
| _getLogValueForBarWidth(barWidth) { | |
| let minBarWidth = (this.minValue / this.maxValue) * this.width; | |
| let maxBarWidth = this.width; | |
| let minValueLog = log(this.minValue); | |
| let maxValueLog = log(this.maxValue); | |
| let scale = (maxValueLog - minValueLog) / (maxBarWidth - minBarWidth); | |
| let value = exp(minValueLog + scale * (barWidth - minBarWidth)); | |
| return value; | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment