Created
December 21, 2016 15:12
-
-
Save spacejack/966e912cfdcd5be494b4de5d70c4e542 to your computer and use it in GitHub Desktop.
m-select ES6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as m from 'mithril'; | |
/** | |
* Standalone event handler that can be bound, then attached | |
* and detached on component remove. | |
*/ | |
function onFocus(el, e) { | |
console.log('global focus event element:', e.target); | |
if (e.target instanceof Node && el.contains(e.target)) { | |
console.log('child focus event'); | |
this.isFocused = true; | |
} | |
} | |
/** | |
* Standalone event handler | |
*/ | |
function onBlur(el, e) { | |
console.log('global blur event element:', e.target); | |
if (e.target instanceof Node && el.contains(e.target)) { | |
console.log('child blur event'); | |
this.isFocused = false; | |
// Elements will blur THEN focus, so there is a moment where none of the | |
// select's elements will be focused. In order to prevent closing the | |
// select in these cases, we delay a frame to be sure no elements | |
// are going to be focused and that the select should really close. | |
requestAnimationFrame(() => { | |
if (this.isOpen && !this.isFocused) { | |
this.isOpen = false; | |
m.redraw(); | |
} | |
}); | |
} | |
} | |
export default { | |
value: undefined, | |
isOpen: false, | |
isFocused: false, | |
focusListener: undefined, | |
blurListener: undefined, | |
dom: undefined, | |
oninit({ attrs: { defaultValue, options } }) { | |
if (defaultValue) { | |
if (typeof defaultValue !== 'string') { | |
console.warn("defaultValue must be a string"); | |
return; | |
} | |
const o = options.find(o => o.value === defaultValue); | |
if (!o) { | |
console.warn("defaultValue does not exist in supplied MSelect Options"); | |
return; | |
} | |
this.value = defaultValue; | |
} | |
}, | |
oncreate({ dom }) { | |
this.focusListener = onFocus.bind(this, dom); | |
window.addEventListener('focus', this.focusListener, true); | |
this.blurListener = onBlur.bind(this, dom); | |
window.addEventListener('blur', this.blurListener, true); | |
this.dom = dom; | |
}, | |
onremove() { | |
window.removeEventListener('focus', this.focusListener, true); | |
window.removeEventListener('blur', this.blurListener, true); | |
}, | |
view({ attrs: { label, options, onSelect, class: klass } }) { | |
let headLabel = label; | |
if (this.value) { | |
headLabel = options.find(o => this.value === o.value).label; | |
} | |
if (!headLabel) { | |
if (options && options.length > 0) { | |
headLabel = options[0].label; | |
} | |
else { | |
headLabel = ''; | |
} | |
} | |
return (m('.m-select', { class: klass }, m('.m-select-head', { | |
tabIndex: '0', | |
onclick: (e) => { | |
if (!this.isOpen) | |
e.preventDefault(); | |
this.toggle(options); | |
console.log("head clicked. isOpen:", this.isOpen); | |
}, | |
onkeyup: (e) => { | |
if (e.keyCode === 32) { | |
this.toggle(options); | |
console.log("head space pressed. isOpen:", this.isOpen); | |
} | |
else if (e.keyCode === 27) { | |
if (this.isOpen) { | |
this.close(); | |
// Re-focus head on close | |
requestAnimationFrame(() => { | |
this.dom.childNodes[0].focus(); | |
}); | |
} | |
} | |
} | |
}, headLabel), m('.m-select-body', { class: this.isOpen ? 'm-select-body-open' : undefined }, m('.m-select-options', options.map((o, index) => m('.m-select-option', { | |
tabIndex: '-1', | |
onclick: (e) => { | |
this.value = o.value; | |
this.isFocused = false; | |
this.close(); | |
// Re-focus head on close | |
requestAnimationFrame(() => { | |
this.dom.childNodes[0].focus(); | |
}); | |
onSelect && onSelect(o.value); | |
}, | |
onkeydown: (e) => { | |
if (e.keyCode === 13) { | |
// Enter selects | |
this.value = o.value; | |
this.isFocused = false; | |
this.close(); | |
// Re-focus head on close | |
requestAnimationFrame(() => { | |
this.dom.childNodes[0].focus(); | |
}); | |
onSelect && onSelect(o.value); | |
} | |
else if (e.keyCode === 27) { | |
// Escape closes | |
this.close(); | |
// Re-focus head on close | |
requestAnimationFrame(() => { | |
this.dom.childNodes[0].focus(); | |
}); | |
} | |
else if (e.keyCode === 37 || e.keyCode === 38) { | |
// Left or up keys - focus previous | |
const i = pmod(index - 1, options.length); | |
const elOpt = this.dom.childNodes[1].childNodes[0].childNodes[i]; | |
console.log(`focusing [${i}]:`, elOpt); | |
// Must delay a frame before focusing | |
requestAnimationFrame(() => { | |
elOpt.focus(); | |
}); | |
} | |
else if (e.keyCode === 39 || e.keyCode === 40) { | |
// Right or down keys - focus next | |
const i = pmod(index + 1, options.length); | |
const elOpt = this.dom.childNodes[1].childNodes[0].childNodes[i]; | |
console.log(`focusing [${i}]:`, elOpt); | |
requestAnimationFrame(() => { | |
elOpt.focus(); | |
}); | |
} | |
} | |
}, o.component ? o.component : o.label || '')))))); | |
}, | |
toggle(options) { | |
if (!this.isOpen) { | |
this.open(options); | |
} | |
else { | |
this.close(); | |
} | |
}, | |
open(options) { | |
this.isOpen = true; | |
// When the component is opened, the idea is to focus | |
// either the currently selected option or the first one | |
// (like a native browser select.) | |
// Then we want to allow the arrow keys to move up & down. | |
if (options && options.length > 0) { | |
let i = 0; | |
if (this.value) { | |
i = options.findIndex(o => o.value === this.value); | |
} | |
else { | |
i = 0; | |
} | |
const elOpt = this.dom.childNodes[1].childNodes[0].childNodes[i]; | |
console.log(`focusing [${i}]:`, elOpt); | |
// Must delay a frame before focusing | |
requestAnimationFrame(() => { | |
elOpt.focus(); | |
}); | |
} | |
}, | |
close() { | |
this.isOpen = false; | |
} | |
}; | |
/** Always positive modulus */ | |
function pmod(n, m) { | |
return ((n % m + m) % m); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment