coveo-slider
Version:
Coveo's enhanced slider component using HTML5 input range
371 lines (320 loc) • 14.6 kB
text/typescript
/// <reference path="Slider.d.ts" />
let CoveoSliderMethods = {
Destroy: 'destroy',
Disable: 'disable',
Enable: 'enable',
Update: 'update',
};
class Slider {
private colors: Colors;
private options: ParsedSliderOptions;
private $el: JQuery;
private _uid: number;
static LowerDefaultColor = '#f57f03';
static UpperDefaultColor = '#dddddd';
static StyleClass = 'coveo-slider-style';
static InputClass = 'coveo-slider-input';
static LabelContainerClass = 'coveo-slider-labels';
static LabelClass = 'coveo-slider-label';
static TickContainerClass = 'coveo-slider-ticks';
static TickClass = 'coveo-slider-tick';
static DefaultValue = 50;
static _uid = 0;
constructor(options: SliderOptions) {
if (options.slider && _.isUndefined(options.min) && options.slider.attr('min')) {
options.min = parseFloat(options.slider.attr('min'));
}
if (options.slider && _.isUndefined(options.max) && options.slider.attr('max')) {
options.max = parseFloat(options.slider.attr('max'));
}
this.options = _.defaults(options, this.defaults(options.colors));
this.$el = options.slider;
this._uid = ++Slider._uid;
this.$el.attr({
'data-uid': this._uid,
max: this.options.max,
min: this.options.min,
step: this.options.step,
});
this.$el.on('input', () => this.onInput());
this.$el.on('change', () => this.onChange());
if (this.$el.prop('disabled')) {
this.disable();
}
this.options.onInit(this);
}
private onInput() {
this.update();
this.options.onSlide(this);
}
private onChange() {
this.update();
this.options.onChange(this);
}
defaults(colors?: Colors): ParsedSliderOptions {
return {
colors: _.defaults(colors || {}, {lower: Slider.LowerDefaultColor, upper: Slider.UpperDefaultColor}),
min: 0,
max: 100,
step: 1,
labels: [],
ticks: [],
value: Slider.DefaultValue,
thumbWidth: 20,
onInit: _.noop,
onSlide: _.noop,
onChange: _.noop,
onDestroy: _.noop,
};
}
update() {
const value = ((this.value - this.min) / (this.max - this.min)) * 100;
this.updateSliderColors(value);
this.positionElements();
}
private positionElements() {
if (this.options.labels && this.options.labels.length) {
const $container = this.$el.nextAll('.' + Slider.LabelContainerClass);
if ($container && $container.length) {
const numberOfTicks = (this.max - this.min) / this.options.step;
_.each(this.options.labels, (label: Label) => {
const $el = $container.find(`.${Slider.LabelClass}[data-index="${label.index}"]`);
if ($el && $el.length) {
const offsetForMiddle = $el.width() / $container.width() * 100 / 2;
const offset = (label.index - this.options.min) / this.options.step / numberOfTicks;
$el.css({
'left': (offset * 100 - offsetForMiddle) + '%',
'margin-left': (offset * -1 * this.options.thumbWidth + (this.options.thumbWidth / 2)) + 'px',
});
}
});
}
}
if (this.options.ticks && this.options.ticks.length) {
const $container = this.$el.nextAll('.' + Slider.TickContainerClass);
if ($container && $container.length) {
const numberOfTicks = (this.max - this.min) / this.options.step;
_.each(this.options.ticks, (tick: number) => {
const $el = $container.find(`.${Slider.TickClass}[data-index="${tick}"]`);
if ($el && $el.length) {
const index = (tick - this.min) / this.options.step;
const offset = index / numberOfTicks;
$el.css({
left: (offset * 100) + '%',
'margin-left': offset * -1 * this.options.thumbWidth + 'px',
background: tick < this.value ? this.options.colors.lower : this.options.colors.upper,
});
}
});
}
}
}
private updateSliderColors(value: number) {
const styleElement = this.$el.prev('.' + Slider.StyleClass);
const colors = this.options.colors;
const gradient = `background: linear-gradient(to right, ${colors.lower} ${value}%, ${colors.upper} ${value}%);`;
const selector = `.${Slider.InputClass}[data-uid='${this._uid}']`;
const webkit = [`input[type="range"]${selector}::-webkit-slider-runnable-track {`, gradient, '}'].join('');
const firefox = [`input[type="range"]${selector}::-moz-range-track {`, gradient, '}'].join('');
const ie = [
`input[type="range"]${selector}::-ms-fill-lower {`,
'background: ',
this.options.colors.lower,
'}',
`input[type="range"]${selector}::-ms-fill-upper {`,
'background: ',
this.options.colors.upper,
'}',
].join('');
styleElement.html([webkit, firefox, ie].join(''));
}
get value(): number {return parseFloat(this.$el.val());}
set value(value: number) {
const minOfValueAndMax = Math.min(value, this.max);
const maxOfValueAndMin = Math.max(minOfValueAndMax, this.min);
this.$el.val(maxOfValueAndMin);
}
get min(): number {return parseFloat(this.$el.attr('min'));}
get max(): number {return parseFloat(this.$el.attr('max'));}
disable() {
this.$el.prop('disabled', true);
this.$el.css('opacity', '0.8');
}
enable() {
this.$el.prop('disabled', false);
this.$el.css('opacity', '1');
}
destroy(elem: JQuery) {
this.$el.prev('.' + Slider.StyleClass).remove();
this.$el.nextAll('.' + Slider.TickContainerClass).remove();
this.$el.nextAll('.' + Slider.LabelContainerClass).remove();
this.$el.removeData('slider');
this.$el.removeAttr('data-uid');
this.$el.off('input change');
elem.removeData('slider');
this.options.onDestroy();
}
}
+function($) {
'use strict';
const createStyleElement = () => $('<style />', {type: 'text/css', class: Slider.StyleClass});
const createInputElement = (value: number, disabled: boolean) => $('<input />',
{type: 'range', class: Slider.InputClass, value: value}).prop('disabled', disabled);
const createTickContainer = () => $('<div />', {class: Slider.TickContainerClass});
const createLabelContainer = () => $('<div />', {class: Slider.LabelContainerClass});
const createLabelElements = (container: JQuery, labels: Label[]) => {
_.chain(labels)
.sortBy((label: Label) => label.index)
.each((label: Label) => {
const $el = $('<div />', {
class: Slider.LabelClass,
text: label.label || label.index,
'data-index': label.index,
});
container.append($el);
});
};
const createTickElements = (container: JQuery, ticks: number[]) => {
_.chain(ticks)
.sortBy(_.identity)
.each((tick: number) => {
const $el = $('<div />', {class: Slider.TickClass, 'data-index': tick});
container.append($el);
});
};
$.fn.slider = function(opts?: SliderOptions | number | string) {
const $this = $(this);
opts = opts || {slider: $this};
let slider: Slider = $this.data('slider');
if (_.isNumber(opts)) {
opts = {value: opts as number};
}
if (slider && slider instanceof Slider) {
const slider: Slider = $this.data('slider');
if (opts && !_.isUndefined((opts as SliderOptions).value)) {
slider.value = (opts as SliderOptions).value;
validateAndCreateElements($this, {value: slider.value});
slider.update();
} else if (_.isString(opts)) {
validateAndCreateElements($this, {});
switch (opts as string) {
case CoveoSliderMethods.Destroy:
slider.destroy($this);
break;
case CoveoSliderMethods.Disable:
slider.disable();
break;
case CoveoSliderMethods.Enable:
slider.enable();
break;
case CoveoSliderMethods.Update:
slider.update();
break;
default:
break;
}
} else {
// no param. update
validateAndCreateElements($this, {value: slider.value});
slider.update();
}
} else if (!_.isString(opts) && !_.isNumber(opts)) {
const sliderOptions = opts as SliderOptions;
const elements = validateAndCreateElements($this, sliderOptions);
sliderOptions.slider = elements.input;
slider = new Slider(sliderOptions);
$this.data('slider', slider);
}
if (slider && slider instanceof Slider) {
slider.update();
}
return $this;
};
function validateAndCreateElements($el: JQuery, opts?: SliderOptions) {
let style: JQuery;
let input: JQuery;
if ($el.is('input[type="range"]')) {
input = $el;
input.prop('disabled', opts.disabled === true);
style = $el.prev('style.' + Slider.StyleClass);
if (style.length == 0) {
style = createStyleElement();
$el.before(style);
}
if (opts && opts.ticks) {
let ticksContainer = $el.nextAll('.' + Slider.TickContainerClass);
const ticks: number[] = _.isBoolean(opts.ticks) && opts.ticks ? _.range(opts.min || 0, (opts.max || 100) + (opts.step || 1),
opts.step || 1)
: opts.ticks as number[];
opts.ticks = ticks;
if (ticks && ticks.length) {
if (ticksContainer.length == 0) {
ticksContainer = createTickContainer();
}
createTickElements(ticksContainer, ticks);
$el.after(ticksContainer);
}
}
if (opts && opts.labels) {
let labelsContainer = $el.nextAll('.' + Slider.LabelContainerClass);
let labels: Array<number | Label> = _.isBoolean(opts.labels) && opts.labels ? _.range(opts.min || 0,
(opts.max || 100) + (opts.step || 1), opts.step || 1) : opts.labels as Label[];
if (labels && labels.length) {
labels = _.map(labels, (label: number | Label): Label => _.isNumber(label) ? {index: label} as Label : label as Label);
if (labelsContainer.length == 0) {
labelsContainer = createLabelContainer();
}
createLabelElements(labelsContainer, labels as Label[]);
$el.after(labelsContainer);
}
opts.labels = labels as Label[];
}
} else {
// assume container
style = $el.children('style.' + Slider.StyleClass);
if (style.length == 0) {
style = createStyleElement();
$el.prepend(style);
}
input = $el.children('input[type="range"].' + Slider.InputClass);
if (input.length == 0) {
input = createInputElement(_.isUndefined(opts.value) ? Slider.DefaultValue : opts.value, opts && opts.disabled === true);
$el.append(input);
} else if (_.isNumber(opts.value)) {
input.val(opts.value);
}
if (opts && opts.ticks) {
let ticksContainer = $el.children('.' + Slider.TickContainerClass);
const ticks: number[] = _.isBoolean(opts.ticks) && opts.ticks ? _.range(opts.min || 0, (opts.max || 100) + (opts.step || 1),
opts.step || 1)
: opts.ticks as number[];
opts.ticks = ticks;
if (ticks && ticks.length) {
if (ticksContainer.length == 0) {
ticksContainer = createTickContainer();
}
createTickElements(ticksContainer, ticks);
$el.append(ticksContainer);
}
}
if (opts && opts.labels) {
let labelsContainer = $el.children('.' + Slider.LabelContainerClass);
let labels: Array<number | Label> = _.isBoolean(opts.labels) && opts.labels ? _.range(opts.min || 0,
(opts.max || 100) + (opts.step || 1), opts.step || 1) : opts.labels as Label[];
if (labels && labels.length) {
labels = _.map(labels, (label: number | Label): Label => _.isNumber(label) ? {index: label} as Label : label as Label);
if (labelsContainer.length == 0) {
labelsContainer = createLabelContainer();
}
createLabelElements(labelsContainer, labels as Label[]);
$el.append(labelsContainer);
}
opts.labels = labels as Label[];
}
}
return {
input: input,
style: style,
};
}
}(jQuery);