UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

724 lines (696 loc) 17.5 kB
(function(){ "use strict"; var DU = Kekule.DomUtils; var EU = Kekule.HtmlElementUtils; var CNS = Kekule.Widget.HtmlClassNames; /** @ignore */ Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, { LISTVIEW: 'K-ListView', LISTVIEW_ITEM_HOLDER: 'K-ListView-ItemHolder', LISTVIEW_ITEM: 'K-ListView-Item', LISTVIEW_ITEMCONTENT: 'K-ListView-ItemContent' }); /** * List view widget, provide the ability to list/select a series of elements inside. * @class * @augments Kekule.Widget.NestedContainer * * @property {Array} items All child list items in list view, array of HTML elements. * @property {Array} selection All selected items in list view. * @property {Object} selectedItem Most recently selected item in tree view. * @property {Bool} enableSelect Whether item selecting is enabled. * @property {Bool} enableMultiSelect Whether only one item can be selected at same time. * @property {Bool} enableSelectInSelection Whether the most active one is allowed to be selected in selection. */ /** * Invoked when the selection is changed in the list view. * event param of it has one fields: {selection: Array, selectedItem: HTMLElement} * @name Kekule.Widget.ListView#selectionChange * @event */ Kekule.Widget.ListView = Class.create(Kekule.Widget.BaseWidget, /** @lends Kekule.Widget.ListView# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.ListView', /** @private */ ITEM_DATA_FIELD: '__$itemData__', /** @construct */ initialize: function(parentOrElementOrDocument) { this.setPropStoreFieldValue('enableSelect', true); this.setPropStoreFieldValue('enableMultiSelect', true); this.tryApplySuper('initialize', [parentOrElementOrDocument]); }, /** @private */ initProperties: function() { this.defineProp('items', {'dataType': DataType.ARRAY, 'serializable': false, 'getter': function() { var result = []; var children = DU.getDirectChildElems(this.getChildrenHolderElement()); for (var i = 0, l = children.length; i < l; ++i) { var child = children[i]; if (this.isChildItemElem(child)) result.push(child); } return result; } }); this.defineProp('enableSelect', { 'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('enableSelect', value); if (!value) this.clearSelection(); } }); this.defineProp('enableMultiSelect', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('enableMultiSelect', value); if (!value) this.clearSelection(); } }); this.defineProp('enableSelectInSelection', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('enableSelectInSelection', value); var curr = this.getSelectedItem(); if (curr) { if (value) { EU.addClass(curr, CNS.STATE_CURRENT_SELECTED); } else { EU.removeClass(curr, CNS.STATE_CURRENT_SELECTED); } } } }); this.defineProp('selection', {'dataType': DataType.ARRAY, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC, 'getter': function() { var result = this.getPropStoreFieldValue('selection'); if (!result) { result = []; this.setPropStoreFieldValue('selection', result); } return result; }, 'setter': function(value) { this.select(value); } }); this.defineProp('currSelectedItem', { 'dataType': DataType.OBJECT, 'serializable': false, 'getter': function() { return this.getSelectedItem(); }, 'setter': function(value) { this.setSelectedItem(value); } }); this.defineProp('selectedItem', {'dataType': DataType.OBJECT, 'serializable': false, 'getter': function() { var selection = this.getSelection(); return selection.length? selection[selection.length - 1]: null; }, 'setter': function(value) { var item = this.getBelongedChildItem(value); if (item) { if (this.getEnableSelectInSelection()) { var selection = this.getSelection(); var index = selection.indexOf(item); if (index >= 0) { this.prepareChangingSelection(); selection.splice(index, 1); selection.push(item); // put item into tail this.selectionChanged(); } else this.select(item); } else this.select(item); } else this.clearSelection(); } }); }, /** @ignore */ doCreateSubElements: function(doc, docFragment) { var result = this.tryApplySuper('doCreateSubElements', []); this._holderElem = doc.createElement('ul'); this._holderElem.className = CNS.LISTVIEW_ITEM_HOLDER; docFragment.appendChild(this._holderElem); result = result.concat(this._holderElem); return result; }, /** @ignore */ getChildrenHolderElement: function() { return this._holderElem; }, /** @ignore */ doGetWidgetClassName: function(/*$super*/) { return this.tryApplySuper('doGetWidgetClassName') + ' ' + CNS.LISTVIEW; }, /** @ignore */ doCreateRootElement: function(doc) { var result = doc.createElement('ul'); return result; }, /** * Returns the HTML class name for all child items. * Descendants can override this method. * @returns {string} */ getItemClassName: function() { return CNS.LISTVIEW_ITEM; }, /** * Create a child item element. * Descendants can override doCreateChildItem to create their own. * @returns {HTMLElement} */ createChildItem: function(data) { var result = this.doCreateChildItem(data); if (result) { EU.addClass(result, this.getItemClassName()); if (data) this.setItemData(result, data); } return result; }, /** @private */ doCreateChildItem: function(data) { var doc = this.getDocument(); var result = doc.createElement('li'); return result; }, /** * Returns data previously set to itemElem. * @param {HTMLElement} item * @returns {Variant} */ getItemData: function(item) { return item[this.ITEM_DATA_FIELD]; }, /** * Set data associated with item. * @param {HTMLElement} item * @param {Variant} data */ setItemData: function(item, data) { this.doSetItemData(item, data); item[this.ITEM_DATA_FIELD] = data; return this; }, /** * Do actual work of method setItemData. * Descendants should override this method. * @param {HTMLElement} item * @param {Variant} data * @private */ doSetItemData: function(item, data) { if (data) { if (data.text) DU.setElementText(item, data.text); if (data.hint) item.setAttribute('title', data.hint); } }, /** * Check if an element is one of the the child item in the list. * @param {HTMLElement} elem * @returns {Bool} */ isChildItemElem: function(elem) { return (elem.parentNode === this.getChildrenHolderElement()) && EU.hasClass(elem, CNS.LISTVIEW_ITEM); }, /** @private */ getBelongedChildItem: function(elem) { if (DU.isDescendantOf(elem, this.getElement())) { if (this.isChildItemElem(elem)) return elem; else return this.getBelongedChildItem(elem.parentNode); } else return null; }, /** * List all child items. * @returns {Array} */ getAllChildItems: function() { var result = []; var children = DU.getDirectChildElems(this.getChildrenHolderElement()); for (var i = 0, l = children.length; i < l; ++i) { var child = children[i]; if (this.isChildItemElem(child)) { result.push(child); } } return result; }, /** * Returns all items between item1 and item2 (including item1 and item2). * @param {HTMLElement} item1 * @param {HTMLElement} item2 * @returns {Array} */ getChildItemElemRange: function(item1, item2) { var allItems = this.getAllChildItems(); var index1 = allItems.indexOf(item1); var index2 = allItems.indexOf(item2); if ((index1 >= 0) && (index2 >= 0)) { if (index1 > index2) { var reversed = true; var temp = index1; index1 = index2; index2 = temp; } var result = allItems.slice(index1, index2 + 1); if (reversed) result = result.reverse(); return result; } else return []; }, /** * Returns the index of item in list. * @param {HTMLElement} elem * @returns {Int} */ indexOfItem: function(elem) { var result = -1; var itemElem = this.getBelongedChildItem(elem); if (itemElem) { var allItems = this.getAllChildItems(); result = allItems.indexOf(itemElem); } return result; }, /** * Returns item element at index. * @param {Int} index * @returns {HTMLElement} */ getItemAt: function(index) { var allItems = this.getAllChildItems(); return allItems[index]; }, /** @private */ _itemBeforeInsert: function(item) { }, /** @private */ _itemInserted: function(item) { EU.addClass(item, this.getItemClassName()); }, /** @private */ _itemBeforeRemove: function(item) { if (this.isItemSelected(item)) this.removeFromSelection(item); EU.removeClass(item, this.getItemClassName()); }, /** @private */ _itemRemoved: function(item) { }, /** * Append an item element to the tail of list. * @param {HTMLElement} item */ appendItem: function(item) { if (!item) item = this.createChildItem(); else if (!DU.isElement(item)) // is hash item = this.createChildItem(item); this._itemBeforeInsert(item); this.getChildrenHolderElement().appendChild(item); this._itemInserted(item); return item; }, /** * Insert an item element before refItem. * @param {HTMLElement} elem * @param {HTMLElement} refElem */ insertItemBefore: function(item, refItem) { if (!item) item = this.createChildItem(); else if (!DU.isElement(item)) // is hash item = this.createChildItem(item); var ref = refItem && this.getBelongedChildItem(refItem); if (!ref) return this.appendItem(item); else { this._itemBeforeInsert(item); this.getChildrenHolderElement().insertBefore(item, ref); this._itemInserted(item); return item; } }, /** * Remove an item element in the list. * @param {HTMLElement} item */ removeItem: function(item) { var elem = this.getBelongedChildItem(item); this._itemBeforeRemove(elem); this.getChildrenHolderElement().removeChild(elem); this._itemRemoved(elem); return item; }, /** * Remove items from list. * @param {Array} items */ removeItems: function(items) { for (var i = items.length - 1; i >= 0; --i) { this.removeItem(items[i]); } return this; }, /** * Clear all child items in list. */ clearItems: function() { this.select(); var items = this.getItems(); this.removeItems(items); return this; }, // methods about selection /** * Check if an item is selected. * @param {HTMLElement} item * @returns {Bool} */ isItemSelected: function(item) { var selection = this.getSelection(); return selection? (selection.indexOf(item) >= 0): false; }, /** * Called before changing selection. * @private */ prepareChangingSelection: function() { this._prevSelectedItem = this.getSelectedItem(); }, /** * Notify the selection of tree view has been just changed. * @private */ selectionChanged: function(added, removed) { var eventParams = { 'selection': this.getSelection(), 'selectedItem': this.getSelectedItem(), 'added': added, 'removed': removed }; var currSelectedItem = this.getSelectedItem(); if (this._prevSelectedItem && this._prevSelectedItem !== currSelectedItem) { eventParams.prevSelectedItem = this._prevSelectedItem; EU.removeClass(this._prevSelectedItem, CNS.STATE_CURRENT_SELECTED); this._prevSelectedItem = null; } if (currSelectedItem && this.getEnableSelectInSelection()) { EU.addClass(currSelectedItem, CNS.STATE_CURRENT_SELECTED); } this.notifyPropSet('selection', this.getSelection()); this.invokeEvent('selectionChange', eventParams); }, /** * Clear all items in selection. */ clearSelection: function() { var selection = this.getSelection(); if (selection.length) { this.prepareChangingSelection(); this.doClearSelection(); this.selectionChanged(null, selection); } return this; }, /** @private */ doClearSelection: function() { var selection = this.getSelection(); if (selection.length) { for (var i = 0, l = selection.length; i < l; ++i) { var item = this.getBelongedChildItem(selection[i]); EU.removeClass(item, [CNS.STATE_SELECTED, CNS.STATE_CURRENT_SELECTED]); } this.setPropStoreFieldValue('selection', []); } }, /** * Remove items from selection. * @param {Variant} items An item element or array of items. */ removeFromSelection: function(items) { if (items) { var removed = []; var removes = Kekule.ArrayUtils.toArray(items); if (removes && removes.length) { var selection = this.getSelection(); this.prepareChangingSelection(); for (var i = 0, l = removes.length; i < l; ++i) { var item = removes[i]; if (Kekule.ArrayUtils.remove(selection, item)) { EU.removeClass(this.getBelongedChildItem(item), [CNS.STATE_SELECTED, CNS.STATE_CURRENT_SELECTED]); removed.push(item); } } this.selectionChanged(null, removed); } } return this; }, /** * Add items to selection. * @param {Variant} items An item element or array of items. */ addToSelection: function(items) { if (items) { var adds = Kekule.ArrayUtils.toArray(items); if (adds && adds.length) { this.prepareChangingSelection(); var added = this.doAddToSelection(adds); this.selectionChanged(added); } } return this; }, /** @private */ doAddToSelection: function(items) { var result = []; var adds = Kekule.ArrayUtils.toArray(items); if (adds && adds.length) { if (!this.getEnableMultiSelect()) { /* this.doClearSelection(); adds = [adds[adds.length - 1]]; */ this.clearSelection(); } { var selection = this.getSelection(); for (var i = 0, l = adds.length; i < l; ++i) { var item = adds[i]; if (!this.isItemSelected(item)) { EU.addClass(this.getBelongedChildItem(item), CNS.STATE_SELECTED); //selection.push(item); selection.splice(selection.length - 1, 0, item); // push before last one, ensure the selectedItem not changed result.push(item); } } } } return result; }, /** * Toggle selection state of items. * @param {Variant} items An item element or array of items. */ toggleSelectionState: function(items) { if (items) { this.beginUpdate(); try { var toggles = Kekule.ArrayUtils.toArray(items); var selection = this.getSelection(); if (toggles && toggles.length) { for (var i = 0, l = toggles.length; i < l; ++i) { var item = toggles[i]; if (this.isItemSelected(item)) this.removeFromSelection(item); else this.addToSelection(item) } } } finally { this.endUpdate(); } } }, /** * Select items in tree view. * @param {Variant} items An item element or array of items. */ select: function(items) { this.beginUpdate(); try { if (this.getSelection().length) { /* if (!items) this.clearSelection(); // since addToSelection will not be called, in here we invoke prepare/done selection methods else { this.doClearSelection(); } */ this.clearSelection(); } if (items) { var objs = Kekule.ArrayUtils.toArray(items); var addedItems = this.getEnableMultiSelect()? objs: objs[objs.length - 1]; this.addToSelection(addedItems); } } finally { this.endUpdate(); } }, // event handlers /** @private */ react_click: function(e) { if (this.getEnableSelect()) { var target = e.getTarget(); var item = this.getBelongedChildItem(target); if (!item) // click on blank place, clear selection { this.select(null); } else if (this.getEnableMultiSelect()) { // check shift and ctrl key state if (e.getShiftKey()) // range select { var selected = this.getSelectedItem(); if (selected) { var range = this.getChildItemElemRange(selected, item); if (range.length) { if (e.getCtrlKey()) this.addToSelection(range); else this.select(range); } } else this.select(item); } else if (e.getCtrlKey()) // toggle selection state { this.toggleSelectionState(item); } else if (item) // select item directly { if (this.getEnableSelectInSelection() && this.isItemSelected(item)) this.setSelectedItem(item); else this.select(item); } } else { this.select(item); } } return this.tryApplySuper('react_click', [e]); } }); })();