selecton
Version:
Selecton.js combines a searchbar and a dropdown menu with nested child lists.
684 lines (524 loc) • 22.9 kB
JavaScript
import {select as d3select} from 'd3-selection';
import {event as d3event} from 'd3-selection';
import {drag as d3drag} from 'd3-drag';
import {default as getItemsByValue} from './src/js/getItemsByValue';
import {default as _dragstart} from './src/js/dragstart';
import {default as _drag} from './src/js/drag';
import {default as _dragend} from './src/js/dragend';
import {default as _hierarchyjump} from './src/js/hierarchyjump';
import {default as _addItemToInput} from './src/js/addItemToInput';
import {default as _removeItemFromInput} from './src/js/removeItemFromInput';
import {default as _makeItemContent} from './src/js/makeItemContent';
import {default as _setHidden} from './src/js/setHidden';
// import {default as _findItemByKey} from './src/js/findItemByKey';
import {default as _sortArraybyArr} from './src/js/sortArraybyArr';
import {default as _setPropertyForAllElements} from './src/js/setPropertyForAllElements';
import {_getInputSearchField} from './src/js/elementHandler';
import {_getInputItems} from './src/js/elementHandler';
import {_getDropdownListitems} from './src/js/elementHandler';
import {_getDropdown} from './src/js/elementHandler';
import {_getDropdownListWrapper} from './src/js/elementHandler';
// import {_openDropdown} from './src/js/dropdownHandler';
// import {_toggleDropdown} from './src/js/dropdownHandler';
export default function(dflts){
var depth = 0;
var dropdown = null;
var el = d3select(dflts.el);
var events = {};
var inputGroup = null;
var itemsInResultSize = 0;
var key = null;
var mode = null;
var keyCount = 0;
var open = null;
var preIconHtml = ' ';
var renderInputItem = null;
var renderDropdownItem = null;
var searchTerm = '';
var showHierarchyButton = null;
var showRemoveButton = null;
var sufIconHtml = '<div><span></span><span></span><span></span><span></span></div>';
var wrapper = null;
var xDragOffset = null;
var yDragOffset = null;
//////////////////////////////////////////
// STORE KEY CODES
//////////////////////////////////////////
var down = 40;
var enter = 13;
var esc = 27;
var tab = 9;
var up = 38;
//////////////////////////////////////////
// DEFINE DEFAULT SETTINGS
//////////////////////////////////////////
var defaults = {
key : function(d){ return d.key; },
open : false,
mode : 'select',
renderInputItem : function(d){ return d.key; },
renderDropdownItem : function(d){ return d.key; },
showHierarchyButton : true,
showRemoveButton : true,
};
//////////////////////////////////////////
// SET SETTINGS
//////////////////////////////////////////
key = ('key' in dflts) ? dflts.key : defaults.key;
mode = ('mode' in dflts) ? dflts.mode : defaults.mode;
open = ('open' in dflts) ? dflts.open : defaults.open;
renderDropdownItem = ('renderDropdownItem' in dflts) ? dflts.renderDropdownItem : defaults.renderDropdownItem;
renderInputItem = ('renderInputItem' in dflts) ? dflts.renderInputItem : defaults.renderInputItem;
showHierarchyButton = ('showHierarchyButton' in dflts) ? dflts.showHierarchyButton : defaults.showHierarchyButton;
showRemoveButton = ('showRemoveButton' in dflts) ? dflts.showRemoveButton : defaults.showHierarchyButton;
//////////////////////////////////////////
// PRIVATE FUNCTIONS
//////////////////////////////////////////
function _init(){
//////////////////////////////////////////
// CLEAR ELEMENT
//////////////////////////////////////////
el.html(null);
//////////////////////////////////////////
// WRAP ELEMENT
//////////////////////////////////////////
wrapper = el
.append('div')
.attr('class', 'selecton selecton-' + mode)
.classed('selecton-is-open', open);
//////////////////////////////////////////
// ADD INPUT GROUP
//////////////////////////////////////////
inputGroup = wrapper
.append('div')
.attr('class', 'selecton-input-group' );
inputGroup
.append('div')
.attr('class', 'selecton-input-group-pre')
.html(preIconHtml)
.on('click', focusSearch);
inputGroup
.append('div')
.attr('class', 'selecton-search')
.on('click', focusSearch)
.append('input')
.attr('class', 'selecton-search-input')
.on('keyup', _handleKeyUpInteraction)
.on('keydown', _preventTabDefault);
inputGroup
.append('div')
.attr('class', 'selecton-ghost')
.attr('contenteditable', 'true')
.style('position', 'absolute')
.style('top', '0')
.style('left', '0')
.style('visibility', 'hidden')
.style('width', 'auto');
if(mode === 'select'){
inputGroup
.append('div')
.html(sufIconHtml)
.attr('class', 'selecton-input-group-suf')
.on('click', _toggleDropdown);
}
//////////////////////////////////////////
// ADD DROPDOWN
//////////////////////////////////////////
dropdown = wrapper
.append('div')
.attr('class', 'selecton-dropdown')
.classed('open', open)
.classed('closed', !open);
}
function _update(data){
if(mode === 'select'){
var list = dropdown.selectAll('ul.list-wrapper').data(data, key);
var enterList = list
.enter()
.append('ul')
.attr('class', 'list-wrapper')
.classed('root-list', true)
.merge(list);
list
.exit()
.remove();
_generateList(enterList);
}
//////////////////////////////////////////
// HANDLE CLICK ON DROPDOWN LIST ITEM
//////////////////////////////////////////
dropdown.selectAll('.dropdown-list-item.dropdown-list-item-selectable span.dropdown-list-item-content').on('click', function(){
d3event.stopPropagation();
var item = this.parentNode.__data__;
_handleMultiselect(item);
item.selected = !item.selected;
_select(this.parentNode);
_updateList();
_emit('change', getSelectedItems());
});
//////////////////////////////////////////
// HANDLE CLICK ON EXPAND TOGGLE
//////////////////////////////////////////
dropdown.selectAll('span.dropdown-list-item-expand-toggle').on('click', function(){
d3event.stopPropagation();
_toggleChildren(this.parentNode.__data__.children);
_updateList();
});
///////////////////////////////////////////////////////////////
// HANDLE CLICK IN HIERACHY BUTTON
///////////////////////////////////////////////////////////////
inputGroup.select('div.selecton-search').selectAll('span.input-item-expand-hierachy-button').on('click', function(){
_closeAllItems();
var itemData = _hierarchyjump.call(this, wrapper, key);
var dropdown = _getDropdown(wrapper);
var dropdownNode = null;
var dropdownHeight = null;
_setSearchInputValue('');
focusSearch();
_updateList();
dropdownHeight = parseInt(dropdown.style('height'));
dropdownNode = _getDropdownListitems(wrapper).filter(function(d){ return key(d) === key(itemData); });
dropdown.node().scrollTop = dropdownNode.node().offsetTop - dropdownHeight / 2;
setTimeout(function() {
itemData._temporarilyHighlighted_ = false;
_updateList();
}, 750);
});
//////////////////////////////////////////
// HANDLE CLICK ON REMOVE BTN IN INPUT
//////////////////////////////////////////
inputGroup.select('div.selecton-search').selectAll('span.input-item-remove-button').on('click', function(){
d3event.stopPropagation();
_removeItemFromInput(wrapper, d3select(this.parentNode).data()[0], {
render: renderInputItem
});
_setSearchInputValue('');
_updateList();
_emit('change', getSelectedItems());
});
//////////////////////////////////////////
// HANDLE DRAG
//////////////////////////////////////////
wrapper.select('div.selecton-search').selectAll('div.input-item span.input-item-content')
.call(d3drag()
.on('start', _dragstarted)
.on('drag', _dragging)
.on('end', _dragended));
}
function _dragstarted(){
_setSearchInputValue('');
var dragOffset = _dragstart.call(this, wrapper);
xDragOffset = dragOffset.x;
yDragOffset = dragOffset.y;
}
function _dragging(){
_drag.call(this, wrapper, xDragOffset, yDragOffset);
}
function _dragended(){
var dragOffset = _dragend.call(this, wrapper, _handleKeyUpInteraction, _preventTabDefault);
xDragOffset = dragOffset.x;
yDragOffset = dragOffset.y;
_emit('change', getSelectedItems());
}
function _generateList(list){
var item = list.selectAll('li.dropdown-list-item').data(function(d){ return [d]; }, key);
var enterItem = item
.enter()
.append('li')
.attr('class', 'dropdown-list-item')
.merge(item)
.each(function(d){
d.closed = 'closed' in d ? d.closed : true;
d.selectable = 'selectable' in d ? d.selectable : true;
d.selected = 'selected' in d ? d.selected : false;
d._temporarilyHighlighted_ = '_temporarilyHighlighted_' in d ? d._temporarilyHighlighted_ : false;
d._depth_ = depth;
})
.classed('temporarily-highlighted', function(d){ return d._temporarilyHighlighted_; })
.classed('dropdown-list-item-selectable', function(d){ return d.selectable; })
.classed('dropdown-list-item-selected', function(d){ return d.selected; })
.classed('dropdown-list-item-hidden',function(d){ return _setHidden(d, searchTerm, renderDropdownItem); })
.html(function(d){ return _makeItemContent(d, searchTerm, renderDropdownItem); });
item.exit().remove();
//////////////////////////////////////////
// ADD CHILDREN
//////////////////////////////////////////
var children = item.merge(enterItem).selectAll('li.dropdown-list-item').data(function(d){ return d.children ? d.children : []; }, key);
//////////////////////////////////////////
// GENERATE NEW LIST IF ITEM HAS CHILDREN
//////////////////////////////////////////
if(!children.enter().empty()){
var itemListWrapper = list.selectAll('ul.list-wrapper').data(function(d){ return d.children ? d.children : []; }, key);
var enterItemListWrapper = itemListWrapper
.enter()
.append('ul')
.attr('class', 'list-wrapper')
.merge(itemListWrapper);
depth++;
_generateList(enterItemListWrapper);
}
children.exit().remove();
}
function _select(el){
_setSearchInputValue('');
if(!el) return;
if(el.__data__.selected){
_addItemToInput(wrapper, el.__data__, {
showRemoveButton : showRemoveButton,
showHierarchyButton : showHierarchyButton,
render : renderInputItem
});
return;
}
_removeItemFromInput(wrapper, el.__data__, {
render: renderInputItem
});
}
function _toggleChildren(elements){
elements.forEach(function(el){
(!el.closed) ? _setPropertyForAllElements(el.children, 'closed', true) : null;
el.closed = !el.closed;
});
}
function _setSearchInputValue(val){
var searchInput = _getInputSearchField(wrapper);
searchInput.style('width', null);
searchInput.node().value = searchTerm = val;
}
function focusSearch(){
var searchInput = _getInputSearchField(wrapper);
searchInput.node().focus();
wrapper.classed('selecton-is-open', true);
dropdown.classed('open', true);
dropdown.classed('closed', false);
}
function _toggleDropdown(){
keyCount = 0;
var isOpen = dropdown.classed('open');
wrapper.classed('selecton-is-open', !isOpen);
dropdown.classed('open', !isOpen).classed('closed', isOpen);
_closeAllItems();
_setSearchInputValue('');
_updateList();
}
function _openDropdown(){
wrapper.classed('selecton-is-open', true);
dropdown.classed('open', true).classed('closed', false);
}
function _handleKeyUpInteraction(){
if(mode === 'add'){
_handleAddMode();
return;
}
if(mode === 'select'){
_handleSelectMode();
}
}
function _handleSelectMode(){
var searchInput = _getInputSearchField(wrapper);
var searchInputNode = searchInput.node();
var inputGhost = inputGroup.select('div.selecton-ghost');
var keyCode = d3event.keyCode || d3event.which;
var itemsInResult = null;
_openDropdown();
searchTerm = searchInputNode.value.toLowerCase();
searchInput.style('width', function(){ return inputGhost.html(searchTerm).node().clientWidth + 20 + 'px'; });
if(keyCode === enter || keyCode === tab){
var selectMe = _getPreselectedItem();
keyCount = 0;
if(selectMe.empty()) return;
_handleMultiselect(selectMe.node().__data__);
selectMe.node().__data__.selected = true;
_addItemToInput(wrapper, selectMe.node().__data__, {
showRemoveButton : showRemoveButton,
showHierarchyButton : showHierarchyButton,
render : renderInputItem
});
_toggleDropdown();
_emit('change', getSelectedItems());
return;
}
_updateList();
if(!(keyCode === up || keyCode === down || keyCode === esc || keyCode === tab || keyCode === enter)){
_emit('keyup', searchTerm);
}
if(keyCode === up){
keyCount--;
}
if(keyCode === down){
keyCount++;
}
if(keyCount < 0){
keyCount = itemsInResultSize + keyCount;
}
if(keyCount > itemsInResultSize - 1){
keyCount = 0;
}
itemsInResult = dropdown.selectAll('ul.list-wrapper li.dropdown-list-item-selectable:not(.dropdown-list-item-hidden)');
itemsInResult.classed('dropdown-list-item-preselected', function(d, i){ return i === keyCount; });
itemsInResultSize = itemsInResult.size();
///////////////////////////////////////////////////////////////
// HANDLE OVERFLOW
///////////////////////////////////////////////////////////////
var preSelectedItem = _getPreselectedItem().node();
var dropdownHeight = parseInt(dropdown.style('height'));
if(preSelectedItem && preSelectedItem.offsetTop + dropdownHeight / 2 > dropdownHeight){
dropdown.node().scrollTop = preSelectedItem.offsetTop - dropdownHeight / 2;
return;
}
dropdown.node().scrollTop = 0;
}
function _handleAddMode(){
var searchInput = _getInputSearchField(wrapper);
var searchInputNode = searchInput.node();
var inputGhost = inputGroup.select('div.selecton-ghost');
var keyCode = d3event.keyCode || d3event.which;
searchTerm = searchInputNode.value;
if(!searchTerm) return;
searchInput.style('width', function(){ return inputGhost.html(searchTerm).node().clientWidth + 20 + 'px'; });
if(keyCode === enter || keyCode === tab){
var sarchTerms = searchTerm.split(',');
sarchTerms.forEach(function(st){
if(st){
_addItemToInput(wrapper, {
selected: true,
key: st.trim()
}, {
showRemoveButton : showRemoveButton,
showHierarchyButton : showHierarchyButton,
render : renderInputItem
});
}
});
_emit('change', getSelectedItems());
_setSearchInputValue('');
_updateList();
return;
}
if(!(keyCode === up || keyCode === down || keyCode === esc || keyCode === tab || keyCode === enter)){
_emit('keyup', searchTerm);
}
}
function _preventTabDefault(){
var keyCode = d3event.keyCode || d3event.which;
(keyCode === tab || keyCode === down || keyCode === up) ? d3event.preventDefault() : null;
}
function _updateList(){
depth = 0;
_update(getData());
}
function _closeAllItems(){
_getDropdownListWrapper(wrapper)
.selectAll('li.dropdown-list-item-selectable')
.each(function(d){ d.closed = true; });
}
function _equalToEventTarget() {
return this === d3event.target;
}
function _getPreselectedItem(){
return dropdown.selectAll('ul.list-wrapper li.dropdown-list-item-selectable:not(.dropdown-list-item-hidden)').filter(function(){ return d3select(this).classed('dropdown-list-item-preselected'); });
}
function _handleMultiselect(item){
var f = _getDropdownListitems(wrapper).filter(function(d){
return key(d) === item._parent_;
}).data()[0];
if(f.multiselect === false){
f.children.forEach(function(c){
if(c.selected === true){
_removeItemFromInput(wrapper, c, {
render: renderInputItem
});
}
c.selected = false;
});
}
}
function _emit(eventName, data) {
if (events[eventName]) {
events[eventName].forEach(function(fn){
fn(data);
});
}
}
//////////////////////////////////////////
// PUBLIC FUNCTIONS
//////////////////////////////////////////
function update(data, order){
_getInputItems(wrapper).remove();
//////////////////////////////////////////
// GET PRESELECTED ITEMS
//////////////////////////////////////////
var preItems = getItemsByValue(data, ['selected', true]);
//////////////////////////////////////////
// SORT ARRAY
//////////////////////////////////////////
if(order){
preItems = _sortArraybyArr(order, preItems);
}
// //////////////////////////////////////////
// // ADD PRESELECTED ITEMS TO INPUT
// //////////////////////////////////////////
preItems.forEach(function(d){
_addItemToInput(wrapper, d, {
showRemoveButton : showRemoveButton,
showHierarchyButton : showHierarchyButton,
render : renderInputItem
});
});
//////////////////////////////////////////
// UPDATE LIST
//////////////////////////////////////////
_update(data);
}
function getData(){
return dropdown.selectAll('div.selecton-dropdown > ul.root-list').data();
}
function on(eventName, fn) {
events[eventName] = events[eventName] || [];
events[eventName].push(fn);
}
function getSelectedItems(){
return _getInputItems(wrapper).data();
}
_init();
//////////////////////////////////////////
// HANDLE OUTSIDE CLICK
//////////////////////////////////////////
d3select(document).on('click touchstart', function(){
var outside = wrapper.selectAll('div.selecton, div.selecton *').filter(_equalToEventTarget).empty();
if (outside && wrapper.classed('selecton-is-open')) {
wrapper.classed('selecton-is-open', false);
dropdown.classed('open', false);
dropdown.classed('closed', true);
_closeAllItems();
_setSearchInputValue('');
_updateList();
}
});
//////////////////////////////////////////
// HANDLE KEYPRESS ON ESC
//////////////////////////////////////////
d3select(document).on('keydown', function(){
var searchInput = _getInputSearchField(wrapper);
var keyCode = d3event.keyCode || d3event.which;
if(keyCode === esc){
keyCount = 0;
searchInput.node().blur();
wrapper.classed('selecton-is-open', false);
dropdown.classed('open', false);
dropdown.classed('closed', true);
_setSearchInputValue('');
_closeAllItems();
_updateList();
return;
}
});
return {
getSelectedItems : getSelectedItems,
getItemsByValue : getItemsByValue,
update : update,
focus : focusSearch,
on : on,
getData : getData
};
}