Search code examples
javascriptsvgsliderinteractive

How can I build slider-functionality within an SVG file?


I have an SVG image of a slider with a colored bar and a handle (see code snippet below). I want to make this SVG image interactive, so the viewer can move the handle to the eleven different positions along the slider. Everything needs to be within the <svg></svg> tags; I can't use external HTML or scripts. The SVG file will be added to an HTML webpage in an <object> tag to preserve interactivity.

Included in the SVG file are eleven invisible <rect> elements to act as hitboxes for each of the eleven slider positions, and a transform="translate(0 0)" tag on the handle group. Clicking in one of the hitboxes should move the handle to the corresponding position by updating the transform parameter's x-value; with the mouse button held down, moving the mouse from side to side should move the handle to the position corresponding to the x-coordinate of the mouse, even if the mouse moves vertically out of range of the hitboxes.

I know you can put JavaScript inside SVG files, but I'm not very familiar with JS so I don't know how to approach this. Is JS the best way? If so, how do I use it to get the desired functionality?

<svg id="Slider-Image" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="84" viewBox="0 0 240 84">
    <defs>
        <style>
            .hitbox {
                cursor: pointer;
                opacity: 0;
            }
        </style>
    </defs>
    <g id="Slider-BG">
        <rect id="BG-Fill" width="240" height="84" fill="#999"/>
        <polygon id="Bevel-Top" points="9 29 14 35 226 35 231 29 9 29" fill="#887"/>
        <polygon id="Bevel-Right" points="226 35 226 63 231 69 231 29 226 35" fill="#aba"/>
        <polygon id="Bevel-Left" points="9 29 9 69 14 63 14 35 9 29" fill="#baa"/>
        <polygon id="Bevel-Bottom" points="9 69 231 69 226 63 14 63 9 69" fill="#cdd"/>
    </g>
    <g id="Slider-Bar">
        <rect id="Slider-Bar-BG" x="14" y="35" width="212" height="28"/>
        <rect id="Slider-Bar-Red" x="15" y="38" width="18" height="19" fill="#f22"/>
        <rect id="Slider-Bar-Yellow" x="35" y="38" width="18" height="19" fill="#ec0"/>
        <rect id="Slider-Bar-Green" x="53" y="38" width="171" height="19" fill="#9e1"/>
        <line id="Slider-Bar-Divider-1" x1="53.5" y1="38" x2="53.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-2" x1="72.5" y1="38" x2="72.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-3" x1="91.5" y1="38" x2="91.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-4" x1="110.5" y1="38" x2="110.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-5" x1="129.5" y1="38" x2="129.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-6" x1="148.5" y1="38" x2="148.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-7" x1="167.5" y1="38" x2="167.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-8" x1="186.5" y1="38" x2="186.5" y2="57" stroke="#000" opacity="0.5"/>
        <line id="Slider-Bar-Divider-9" x1="205.5" y1="38" x2="205.5" y2="57" stroke="#000" opacity="0.5"/>
    </g>
    <g id="Slider-Handle" transform="translate(0 0)">
        <polygon id="Handle-Body" points="31 66 57 66 57 46 46 35 42 35 31 46 31 66" fill="#aaa"/>
        <rect id="Handle-Center" x="42" y="35" width="4" height="31" fill="#ddd"/>
        <rect id="Handle-Pointer" x="43" y="34" width="2" height="16" fill="#111"/>
    </g>
    <g id="Slider-Positions">
        <rect id="Slider-Position-Minus-20" class="hitbox" x="15" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-0" class="hitbox" x="34" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-19" class="hitbox" x="53" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-38" class="hitbox" x="72" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-57" class="hitbox" x="91" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-76" class="hitbox" x="110" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-95" class="hitbox" x="129" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-114" class="hitbox" x="148" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-133" class="hitbox" x="167" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-152" class="hitbox" x="186" y="29" width="19" height="40"/>
        <rect id="Slider-Position-Plus-171" class="hitbox" x="205" y="29" width="19" height="40"/>
    </g>
</svg>


Solution

  • It is something like this. The code should look a bit different when it is an independent document, but you get the idea.

    console.log(Math.ceil((200 - 53) / 19) * 19);
    <svg id="Slider-Image" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="84" viewBox="0 0 240 84">
      <defs>
        <style>
          #Slider-Hitbox {
            cursor: pointer;
            opacity: 0;
          }
          .divider {
            stroke: #000;
            opacity: 0.5;
          }
        </style>
      </defs>
      <g id="Slider-BG">
        <rect id="BG-Fill" width="240" height="84" fill="#999"/>
        <polygon id="Bevel-Top" points="9 29 14 35 226 35 231 29 9 29" fill="#887"/>
        <polygon id="Bevel-Right" points="226 35 226 63 231 69 231 29 226 35" fill="#aba"/>
        <polygon id="Bevel-Left" points="9 29 9 69 14 63 14 35 9 29" fill="#baa"/>
        <polygon id="Bevel-Bottom" points="9 69 231 69 226 63 14 63 9 69" fill="#cdd"/>
      </g>
      <g id="Slider-Bar">
        <rect id="Slider-Bar-BG" x="14" y="35" width="212" height="28"/>
        <rect id="Slider-Bar-Red" x="15" y="38" width="18" height="19" fill="#f22"/>
        <rect id="Slider-Bar-Yellow" x="35" y="38" width="18" height="19" fill="#ec0"/>
        <rect id="Slider-Bar-Green" x="53" y="38" width="171" height="19" fill="#9e1"/>
        <line id="Slider-Bar-Divider-1" class="divider" x1="53.5" y1="38" x2="53.5" y2="57"/>
        <line id="Slider-Bar-Divider-2" class="divider" x1="72.5" y1="38" x2="72.5" y2="57"/>
        <line id="Slider-Bar-Divider-3" class="divider" x1="91.5" y1="38" x2="91.5" y2="57"/>
        <line id="Slider-Bar-Divider-4" class="divider" x1="110.5" y1="38" x2="110.5" y2="57"/>
        <line id="Slider-Bar-Divider-5" class="divider" x1="129.5" y1="38" x2="129.5" y2="57"/>
        <line id="Slider-Bar-Divider-6" class="divider" x1="148.5" y1="38" x2="148.5" y2="57"/>
        <line id="Slider-Bar-Divider-7" class="divider" x1="167.5" y1="38" x2="167.5" y2="57"/>
        <line id="Slider-Bar-Divider-8" class="divider" x1="186.5" y1="38" x2="186.5" y2="57"/>
        <line id="Slider-Bar-Divider-9" class="divider" x1="205.5" y1="38" x2="205.5" y2="57"/>
      </g>
      <g id="Slider-Handle" transform="translate(0 0)">
        <polygon id="Handle-Body" points="31 66 57 66 57 46 46 35 42 35 31 46 31 66" fill="#aaa"/>
        <rect id="Handle-Center" x="42" y="35" width="4" height="31" fill="#ddd"/>
        <rect id="Handle-Pointer" x="43" y="34" width="2" height="16" fill="#111"/>
      </g>
      <rect id="Slider-Hitbox" x="13" y="34" width="214" height="30"/>
      <script>//<![CDATA[
        var down = false;
        var translateX = 0;
        const toSVGPoint = (svg, x, y) => {
          let p = new DOMPoint(x, y);
          return p.matrixTransform(svg.getScreenCTM().inverse());
        };
        document.getElementById("Slider-Hitbox").addEventListener("mousedown", e => {
          down = true;
          let p = toSVGPoint(document.getElementById("Slider-Image"), e.clientX, e.clientY);
          let translateX = Math.ceil((p.x-53) / 19) * 19;
          if(translateX < 0) translateX = -20;
          if(translateX > 171) translateX = 171;
          document.getElementById("Slider-Handle").setAttribute("transform", `translate(${translateX} 0)`);
        });
        document.getElementById("Slider-Image").addEventListener("mousemove", e => {
          if(down) {
            let p = toSVGPoint(document.getElementById("Slider-Image"), e.clientX, e.clientY);
            let translateX = Math.ceil((p.x-53) / 19) * 19;
            if(translateX < 0) translateX = -20;
            if(translateX > 171) translateX = 171;
            document.getElementById("Slider-Handle").setAttribute("transform", `translate(${translateX} 0)`);
          }
        });
        document.getElementById("Slider-Image").addEventListener("mouseup", e => {
          down = false;
        });
      //]]></script>
    </svg>

    This is the entire SVG with JavaScript running in an object element:

    <object width="240" data="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyBpZD0iU2xpZGVyLUltYWdlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQwIiBoZWlnaHQ9Ijg0IiB2aWV3Qm94PSIwIDAgMjQwIDg0Ij4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgI1NsaWRlci1IaXRib3ggewogICAgICAgIGN1cnNvcjogcG9pbnRlcjsKICAgICAgICBvcGFjaXR5OiAwOwogICAgICB9CiAgICAgIC5kaXZpZGVyIHsKICAgICAgICBzdHJva2U6ICMwMDA7CiAgICAgICAgb3BhY2l0eTogMC41OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8ZyBpZD0iU2xpZGVyLUJHIj4KICAgIDxyZWN0IGlkPSJCRy1GaWxsIiB3aWR0aD0iMjQwIiBoZWlnaHQ9Ijg0IiBmaWxsPSIjOTk5Ii8+CiAgICA8cG9seWdvbiBpZD0iQmV2ZWwtVG9wIiBwb2ludHM9IjkgMjkgMTQgMzUgMjI2IDM1IDIzMSAyOSA5IDI5IiBmaWxsPSIjODg3Ii8+CiAgICA8cG9seWdvbiBpZD0iQmV2ZWwtUmlnaHQiIHBvaW50cz0iMjI2IDM1IDIyNiA2MyAyMzEgNjkgMjMxIDI5IDIyNiAzNSIgZmlsbD0iI2FiYSIvPgogICAgPHBvbHlnb24gaWQ9IkJldmVsLUxlZnQiIHBvaW50cz0iOSAyOSA5IDY5IDE0IDYzIDE0IDM1IDkgMjkiIGZpbGw9IiNiYWEiLz4KICAgIDxwb2x5Z29uIGlkPSJCZXZlbC1Cb3R0b20iIHBvaW50cz0iOSA2OSAyMzEgNjkgMjI2IDYzIDE0IDYzIDkgNjkiIGZpbGw9IiNjZGQiLz4KICA8L2c+CiAgPGcgaWQ9IlNsaWRlci1CYXIiPgogICAgPHJlY3QgaWQ9IlNsaWRlci1CYXItQkciIHg9IjE0IiB5PSIzNSIgd2lkdGg9IjIxMiIgaGVpZ2h0PSIyOCIvPgogICAgPHJlY3QgaWQ9IlNsaWRlci1CYXItUmVkIiB4PSIxNSIgeT0iMzgiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOSIgZmlsbD0iI2YyMiIvPgogICAgPHJlY3QgaWQ9IlNsaWRlci1CYXItWWVsbG93IiB4PSIzNSIgeT0iMzgiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOSIgZmlsbD0iI2VjMCIvPgogICAgPHJlY3QgaWQ9IlNsaWRlci1CYXItR3JlZW4iIHg9IjUzIiB5PSIzOCIgd2lkdGg9IjE3MSIgaGVpZ2h0PSIxOSIgZmlsbD0iIzllMSIvPgogICAgPGxpbmUgaWQ9IlNsaWRlci1CYXItRGl2aWRlci0xIiBjbGFzcz0iZGl2aWRlciIgeDE9IjUzLjUiIHkxPSIzOCIgeDI9IjUzLjUiIHkyPSI1NyIvPgogICAgPGxpbmUgaWQ9IlNsaWRlci1CYXItRGl2aWRlci0yIiBjbGFzcz0iZGl2aWRlciIgeDE9IjcyLjUiIHkxPSIzOCIgeDI9IjcyLjUiIHkyPSI1NyIvPgogICAgPGxpbmUgaWQ9IlNsaWRlci1CYXItRGl2aWRlci0zIiBjbGFzcz0iZGl2aWRlciIgeDE9IjkxLjUiIHkxPSIzOCIgeDI9IjkxLjUiIHkyPSI1NyIvPgogICAgPGxpbmUgaWQ9IlNsaWRlci1CYXItRGl2aWRlci00IiBjbGFzcz0iZGl2aWRlciIgeDE9IjExMC41IiB5MT0iMzgiIHgyPSIxMTAuNSIgeTI9IjU3Ii8+CiAgICA8bGluZSBpZD0iU2xpZGVyLUJhci1EaXZpZGVyLTUiIGNsYXNzPSJkaXZpZGVyIiB4MT0iMTI5LjUiIHkxPSIzOCIgeDI9IjEyOS41IiB5Mj0iNTciLz4KICAgIDxsaW5lIGlkPSJTbGlkZXItQmFyLURpdmlkZXItNiIgY2xhc3M9ImRpdmlkZXIiIHgxPSIxNDguNSIgeTE9IjM4IiB4Mj0iMTQ4LjUiIHkyPSI1NyIvPgogICAgPGxpbmUgaWQ9IlNsaWRlci1CYXItRGl2aWRlci03IiBjbGFzcz0iZGl2aWRlciIgeDE9IjE2Ny41IiB5MT0iMzgiIHgyPSIxNjcuNSIgeTI9IjU3Ii8+CiAgICA8bGluZSBpZD0iU2xpZGVyLUJhci1EaXZpZGVyLTgiIGNsYXNzPSJkaXZpZGVyIiB4MT0iMTg2LjUiIHkxPSIzOCIgeDI9IjE4Ni41IiB5Mj0iNTciLz4KICAgIDxsaW5lIGlkPSJTbGlkZXItQmFyLURpdmlkZXItOSIgY2xhc3M9ImRpdmlkZXIiIHgxPSIyMDUuNSIgeTE9IjM4IiB4Mj0iMjA1LjUiIHkyPSI1NyIvPgogIDwvZz4KICA8ZyBpZD0iU2xpZGVyLUhhbmRsZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAwKSI+CiAgICA8cG9seWdvbiBpZD0iSGFuZGxlLUJvZHkiIHBvaW50cz0iMzEgNjYgNTcgNjYgNTcgNDYgNDYgMzUgNDIgMzUgMzEgNDYgMzEgNjYiIGZpbGw9IiNhYWEiLz4KICAgIDxyZWN0IGlkPSJIYW5kbGUtQ2VudGVyIiB4PSI0MiIgeT0iMzUiIHdpZHRoPSI0IiBoZWlnaHQ9IjMxIiBmaWxsPSIjZGRkIi8+CiAgICA8cmVjdCBpZD0iSGFuZGxlLVBvaW50ZXIiIHg9IjQzIiB5PSIzNCIgd2lkdGg9IjIiIGhlaWdodD0iMTYiIGZpbGw9IiMxMTEiLz4KICA8L2c+CiAgPHJlY3QgaWQ9IlNsaWRlci1IaXRib3giIHg9IjEzIiB5PSIzNCIgd2lkdGg9IjIxNCIgaGVpZ2h0PSIzMCIvPgogIDxzY3JpcHQ+Ly88IVtDREFUQVsKICAgIHZhciBkb3duID0gZmFsc2U7CiAgICB2YXIgdHJhbnNsYXRlWCA9IDA7CiAgICBjb25zdCB0b1NWR1BvaW50ID0gKHN2ZywgeCwgeSkgPT4gewogICAgICBsZXQgcCA9IG5ldyBET01Qb2ludCh4LCB5KTsKICAgICAgcmV0dXJuIHAubWF0cml4VHJhbnNmb3JtKHN2Zy5nZXRTY3JlZW5DVE0oKS5pbnZlcnNlKCkpOwogICAgfTsKICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJTbGlkZXItSGl0Ym94IikuYWRkRXZlbnRMaXN0ZW5lcigibW91c2Vkb3duIiwgZSA9PiB7CiAgICAgIGRvd24gPSB0cnVlOwogICAgICBsZXQgcCA9IHRvU1ZHUG9pbnQoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoIlNsaWRlci1JbWFnZSIpLCBlLmNsaWVudFgsIGUuY2xpZW50WSk7CiAgICAgIGxldCB0cmFuc2xhdGVYID0gTWF0aC5jZWlsKChwLngtNTMpIC8gMTkpICogMTk7CiAgICAgIGlmKHRyYW5zbGF0ZVggPCAtMTkpIHRyYW5zbGF0ZVggPSAtMTk7CiAgICAgIGlmKHRyYW5zbGF0ZVggPiAxNzEpIHRyYW5zbGF0ZVggPSAxNzE7CiAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJTbGlkZXItSGFuZGxlIikuc2V0QXR0cmlidXRlKCJ0cmFuc2Zvcm0iLCBgdHJhbnNsYXRlKCR7dHJhbnNsYXRlWH0gMClgKTsKICAgIH0pOwogICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoIlNsaWRlci1JbWFnZSIpLmFkZEV2ZW50TGlzdGVuZXIoIm1vdXNlbW92ZSIsIGUgPT4gewogICAgICBpZihkb3duKSB7CiAgICAgICAgbGV0IHAgPSB0b1NWR1BvaW50KGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJTbGlkZXItSW1hZ2UiKSwgZS5jbGllbnRYLCBlLmNsaWVudFkpOwogICAgICAgIGxldCB0cmFuc2xhdGVYID0gTWF0aC5jZWlsKChwLngtNTMpIC8gMTkpICogMTk7CiAgICAgICAgaWYodHJhbnNsYXRlWCA8IC0xOSkgdHJhbnNsYXRlWCA9IC0xOTsKICAgICAgICBpZih0cmFuc2xhdGVYID4gMTcxKSB0cmFuc2xhdGVYID0gMTcxOwogICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJTbGlkZXItSGFuZGxlIikuc2V0QXR0cmlidXRlKCJ0cmFuc2Zvcm0iLCBgdHJhbnNsYXRlKCR7dHJhbnNsYXRlWH0gMClgKTsKICAgICAgfQogICAgfSk7CiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiU2xpZGVyLUltYWdlIikuYWRkRXZlbnRMaXN0ZW5lcigibW91c2V1cCIsIGUgPT4gewogICAgICBkb3duID0gZmFsc2U7CiAgICB9KTsKICAvL11dPjwvc2NyaXB0Pgo8L3N2Zz4="></object>