Skip to content

Instantly share code, notes, and snippets.

@jarek-foksa
Created December 31, 2013 01:51
Show Gist options
  • Select an option

  • Save jarek-foksa/8191160 to your computer and use it in GitHub Desktop.

Select an option

Save jarek-foksa/8191160 to your computer and use it in GitHub Desktop.
// @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