UNPKG

selecton

Version:

Selecton.js combines a searchbar and a dropdown menu with nested child lists.

684 lines (524 loc) 22.9 kB
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 = '&nbsp;'; 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 }; }