src/Slider.ts

   1/**

   2 * # Slider Widget

   3 * Shows a slider that can select a continuous or nominal value out of a range

   4 * 

   5 * ### Profile

   6 * invoked as `m(Slider, {

   7 *      range: [number,number] | string[],

   8 *      onchange: (v:number|string) => void

   9 * });`

  10 * 

  11 * ### Attributes (node.attrs):

  12 * - `onchange:(v:number|string) => void` function to execute when the slider has clicked

  13 * - `range: [number,number] | string[]` range of values the slider can have; either continuous or nomninal

  14 * - `css: string` css class to assign to button tag

  15 * 

  16 * ### Example

  17 * 

  18 * 'script.js'>

  19 * let clicked = 0;

  20 * let radio = '';

  21 * let toggle = '';

  22 * 

  23 * let nom;

  24 * let con;

  25 * 

  26 * m.mount(root, {view: () => m('', [

  27 *   m('h4', `Nominal Slider: ${nom}`),

  28 *   m(hsWidget.Slider, {

  29 *      range: ['one''two''three'],

  30 *      onchange: v => nom=v

  31 *   }),

  32 *   m('h4', `Continuous Slider: ${con}`),

  33 *   m(hsWidget.Slider, {

  34 *      range: [0, 100],

  35 *      onchange: v => con=Math.floor(v*10)/10

  36 *   })

  37 * ])});

  38 * 

  39 * 

  40 */

  41

  42/** */

  43import { m, Vnode } from 'hslayout'

  44

  45

  46type SliderRange = Array;

  47

  48/**

  49 * # Slider Widget

  50 * Shows a slider that can select a continuous or nominal value out of a range

  51 * 

  52 * ### Profile

  53 * invoked as `m(Slider, {

  54 *      range: [number,number] | string[],

  55 *      onchange: (v:number|string) => void

  56 * });`

  57 * 

  58 * ### Attributes (node.attrs):

  59 * - `onchange:(v:number|string) => void` function to execute when the slider has changed

  60 * - `range: [number,number] | string[]` range of values the slider can have; either continuous or nomninal

  61 * - `css: string` css class to assign to button tag

  62 */

  63export class Slider {

  64    oninit(node:Vnode) {

  65        node.state.range = [];

  66        node.state.value = 0.5;     // reflects the slider position, 0...1

  67        node.state.mouse = -1;      // <0: inactive; 0...n pixel: active

  68        node.state.slider = 0;       // 0...1 last slider position

  69        node.state.notified = '';    // last notifed value

  70        node.state.onchange = () => {};

  71}

  72    view(node: Vnode): Vnode { 

  73        const id = node.attrs.id;

  74        const css = node.attrs.css || '';

  75        node.state.range = node.attrs.range || [];

  76        node.state.onchange = node.attrs.onchange;

  77        return m(`.hs-slider ${css}`, {

  78            id:id,

  79            onmousedown:(e:any) => mousedown(e, node), 

  80            onmousemove:(e:any) => mousemove(e, node), 

  81            onmouseup:(e:any)   => mouseup(e, node),

  82            onmouseout:(e:any)  => mouseout(e, node)

  83        },

  84        [renderSlider(node)]);

  85    }

  86

  87    

  88}

  89

  90function renderSlider(node:Vnode): Vnode {

  91    return m('.hs-slider-slot', [

  92        m('.hs-slider-markers', node.state.range.map(renderMarker)), 

  93        m('.hs-slider-handle', { style: `left:${100*node.state.value}%` })

  94    ]);

  95}

  96

  97function renderMarker(value: number|string, i:number, markers:SliderRange):Vnode {

  98    const share = i / (markers.length-1); // pos (0...1) of marker along slider

  99    const left = markers.length<2? 0 : 100*share;

 100    return m('.hs-slider-marker', {style: `left: ${left}%`}, renderLabel(value));

 101}

 102

 103function renderLabel(value: number|string):Vnode {

 104    return m('.hs-slider-label', value);

 105}

 106

 107

 108

 109function getTargetOffset(e:any):number {

 110    let target:any = e.target;

 111    let leftOffset = 0;

 112    while (target.className.trim() !== e.currentTarget.className.trim()) {

 113        leftOffset += target.offsetLeft;

 114        target = target.parentNode;

 115    }

 116    return leftOffset - target.lastChild.offsetLeft;

 117}

 118

 119function getValue(e:any, node:Vnode) {

 120    e.stopPropagation();

 121    e.preventDefault();

 122    const slotWidth = e.currentTarget.lastChild.clientWidth;

 123    node.state.value = (e.clientX - node.state.mouse) / slotWidth + node.state.slider;

 124    return notify(node);

 125}

 126

 127function mousedown(e:any, node:Vnode) { 

 128    const offset = getTargetOffset(e);

 129    node.state.mouse = e.clientX;

 130    if (['hs-slider''hs-slider-slot'].indexOf(e.target.className.trim())>=0) { 

 131        const slotWidth = e.currentTarget.lastChild.clientWidth;

 132        const handleWidth = e.currentTarget.lastChild.lastChild.clientWidth;

 133        node.state.mouse -= handleWidth/2;

 134        node.state.value = (e.offsetX - handleWidth/2 + offset) / slotWidth; 

 135    }

 136    node.state.slider = node.state.value;

 137    getValue(e, node);

 138}

 139

 140function mousemove(e:any, node:Vnode)   { 

 141    if (node.state.mouse>0) {

 142        getValue(e, node);

 143        if (node.state.value > 1 || node.state.value < 0) { mouseup(e, node); }

 144    }

 145}

 146

 147function mouseup(e:any, node:Vnode)   { 

 148    if (node.state.mouse>0) {

 149        node.state.value = getValue(e, node);

 150        node.state.mouse = -1;

 151    }

 152}

 153

 154function mouseout(e:any, node:Vnode) {

 155    if (node.state.mouse>0 && e.target.className.trim() === 'hs-slider') {

 156        mouseup(e, node);

 157    }

 158}

 159

 160function notify(node:Vnode):number {

 161    if ((node.state.range.length > 1) && (typeof node.state.range[0] ==='string')) {

 162        const v = Math.floor(node.state.value * (node.state.range.length-1) + 0.5);

 163        if (node.state.notified !== node.state.range[v]) {

 164            node.state.onchange(node.state.range[v]); // notify change hook

 165            node.state.notified = node.state.range[v];

 166        }

 167        // return a snap to valid value

 168        return v / (node.state.range.length-1);

 169    } else {

 170        const numRange = <[number, number]>node.state.range;

 171        const v = Math.floor((numRange[0]*(1-node.state.value) + numRange[1]*node.state.value)*100)/100;

 172        node.state.onchange(Math.min(node.state.range[1], Math.max(node.state.range[0], v)));

 173        return node.state.value;

 174    }

 175}