Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Customizable spin box</title>
  <script>
    /*
     * A generic spin box component.
     *
     * This is very basic, and omits a lot of details (e.g., accessibility,
     * events) that would be needed in a real spin box.
     */
    // Key for storing the tag of the element used for spin buttons.
    const buttonTagKey = Symbol();
    // Key for storing the spin box's value.
    const valueKey = Symbol();
    class SpinBox extends HTMLElement {
      constructor() {
        super();
        this.value = 0;
        this.defaultButtonTag = 'button';
      }
      attributeChangedCallback(attributeName, oldValue, newValue) {
        switch (attributeName) {
          case 'button-tag':
            this.buttonTag = newValue;
            break;
          case 'value':
            this.value = newValue;
            break;
        }
      }
      get buttonTag() {
        return this[buttonTagKey] || this.defaultButtonTag;
      }
      set buttonTag(buttonTag) {
        this[buttonTagKey] = buttonTag;
      }
      connectedCallback() {
        // Populate the element's shadow.
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = this.template;
        // Wire up event handlers.
        const upButton = this.shadowRoot.getElementById('upButton');
        const downButton = this.shadowRoot.getElementById('downButton');
        upButton.addEventListener('mousedown', () => { this.value++; });
        downButton.addEventListener('mousedown', () => { this.value--; });
      }
      
      static get observedAttributes() {
        return ['button-tag', 'value'];
      }
      // Return a template for the element.
      // This incorporates the current button tag.
      get template() {
        const buttonTag = this.buttonTag;
        return `
          <style>
            :host {
              border: 1px solid gray;
              display: inline-flex;
              min-width: 2.5em;
            }
            #value {
              flex: 1;
              text-align: right;
            }
            #value {
              margin-right: 0.25em;
            }
            #buttons {
              display: inline-flex;
              flex-direction: column;
              -webkit-user-select: none;
              user-select: none;
              width: 0.5em;
            }
            .spinButton {
              flex: 1;
              font-size: 0.25em;
              padding: 0;
            }
          </style>
          <span id="value">${this.value}</span>
          <div id="buttons">
            <${buttonTag} id="upButton" class="spinButton">▲</${buttonTag}>
            <${buttonTag} id="downButton" class="spinButton">▼</${buttonTag}>
          </div>
        `;
      }
      get value() {
        return this[valueKey];
      }
      set value(value) {
        this[valueKey] = value;
        if (this.shadowRoot) {
          // Render the current value into the shadow.
          this.shadowRoot.getElementById('value').textContent = value;
        }
      }
    }
    customElements.define('spin-box', SpinBox);
    /*
     * A rudimentary custom button that just defines some basic styling.
     */
    class CustomButton extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
          <style>
            :host {
              display: inline-flex;
            }
            button {
              border: 1px solid gray;
              background: darkgray;
              color: white;
              font-family: inherit;
              font-size: inherit;
              flex: 1;
              padding: 0;
            }
          </style>
          <button><slot></slot></button>
        `;
      }
    }
    customElements.define('custom-button', CustomButton);
    /*
     * A more advanced custom button that generates repeated 'mousedown' events
     * for as long as it's held down. This approaches real spin box button
     * behavior.
     */
    const initialTimeoutDuration = 500; // Wait a bit before starting repeats.
    const repeatIntervalDuration = 50;  // Once repeats start, they go fast.
    // Key for storing initial timeout delay after button is first pressed.
    const initialTimeoutKey = Symbol();
    // Key for storing interval raised while button is held down.
    const repeatIntervalKey = Symbol();
    class RepeatButton extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' });
        // CSS note: Buttons use both height/width and flex because iOS doesn't
        // want to draw a tiny button.
        this.shadowRoot.innerHTML = `
          <style>
            :host {
              display: inline-flex;
              position: relative;
              -webkit-user-select: none;
              user-select: none;
            }
            button {
              flex: 1;
              font-size: inherit;
              height: 100%;
              width: 100%;
              padding: 0;
            }
          </style>
          <button><slot></slot></button>
        `;
        // Wire up event handlers.
        const button = this.shadowRoot.querySelector('button');
        button.addEventListener('mousedown', () => { this.repeatStart(); });
        button.addEventListener('mouseup', () => { this.repeatStop(); });
        button.addEventListener('mouseleave', () => { this.repeatStop(); });
        // Treat touch events like mouse events.
        button.addEventListener('touchstart', () => { this.repeatStart(); });
        button.addEventListener('touchend', () => { this.repeatStop(); });
      }
      repeatStart() {
        // Start initial wait.
        this[initialTimeoutKey] = setTimeout(() => {
          // Initial wait complete; start repeat interval.
          this[repeatIntervalKey] = setInterval(() => {
            // Repeat interval passed; raise a mousedown event.
            this.raiseMousedown();
          }, repeatIntervalDuration);
        }, initialTimeoutDuration - repeatIntervalDuration);
      }
      repeatStop() {
        // Stop timeout and/or interval in progress.
        if (this[initialTimeoutKey]) {
          clearTimeout(this[initialTimeoutKey]);
          this[initialTimeoutKey] = null;
        }
        if (this[repeatIntervalKey]) {
          clearInterval(this[repeatIntervalKey]);
          this[repeatIntervalKey] = null;
        }
      }
      // Raise a synthetic mousedown event.
      raiseMousedown() {
        const event = new MouseEvent('mousedown', {
          bubbles: true,
          cancelable: true,
          clientX: 0,
          clientY: 0,
          button: 0
        });
        this.dispatchEvent(event);
      }
    }
    customElements.define('repeat-button', RepeatButton);
  </script>
  <style>
    body {
      font-size: 36px;
      padding: 1em;
    }
  </style>
</head>
<body>
  <p>Spin box with plain buttons:</p>
  <spin-box></spin-box>
  <p>A custom button on its own:</p>
  <custom-button>Custom button</custom-button>
  <p>Spin box using that custom button for the arrows:</p>
  <spin-box button-tag="custom-button"></spin-box>
  <p>Spin box with button elements that repeat when held down:</p>
  <spin-box button-tag="repeat-button"></spin-box>
</body>
</html>
Output 300px

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
Jan_Miksovskypro
0viewers