Skip to content

Instantly share code, notes, and snippets.

@spacejack
Created December 21, 2016 15:12
Show Gist options
  • Save spacejack/966e912cfdcd5be494b4de5d70c4e542 to your computer and use it in GitHub Desktop.
Save spacejack/966e912cfdcd5be494b4de5d70c4e542 to your computer and use it in GitHub Desktop.
m-select ES6
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