<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
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |