UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

712 lines (678 loc) 23.4 kB
/** * @fileoverview * Implementation of periodic table of elements. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /widgets/kekule.Widget.base.js * requires /widgets/chem/kekule.chemWidget.base.js * requires /data/kekule.chemicalElementsData.js * requires /data/kekule.isotopesData.js */ (function(){ "use strict"; var DU = Kekule.DomUtils; var EU = Kekule.HtmlElementUtils; //var CWT = Kekule.ChemWidgetTexts; var CNS = Kekule.Widget.HtmlClassNames; var CCNS = Kekule.ChemWidget.HtmlClassNames; /** @ignore */ Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, { PERIODIC_TABLE: 'K-Chem-Periodic-Table', PERIODIC_TABLE_MINI: 'K-Chem-Periodic-Table-Mini', PERIODIC_TABLE_LEGEND: 'K-Chem-Periodic-Table-Legend', PERIODIC_TABLE_LEGEND_CONTENT: 'K-Chem-Periodic-Table-Legend-Content', PERIODIC_TABLE_LEGEND_COLORS: 'K-Chem-Periodic-Table-Legend-Colors', PERIODIC_TABLE_LEGEND_COLOR: 'K-Chem-Periodic-Table-Legend-Color', PERIODIC_TABLE_MAINTABLE: 'K-Chem-Periodic-Table-MainTable', PERIODIC_TABLE_EXTRATABLE: 'K-Chem-Periodic-Table-ExtraTable', PERIODIC_TABLE_ELEM_CELL: 'K-Chem-Periodic-Table-Elem-Cell', PERIODIC_TABLE_ELEM_STUBSCELL: 'K-Chem-Periodic-Table-Elem-StubsCell', PERIODIC_TABLE_ELEM_CELL_CONTENT: 'K-Chem-Periodic-Table-Elem-Cell-Content', PERIODIC_TABLE_LEGEND_ELEM_CELL_CONTENT: 'K-Chem-Periodic-Table-Elem-Cell-Content', PERIODIC_TABLE_HEAD_CELL: 'K-Chem-Periodic-Table-Head-Cell', PERIODIC_TABLE_HEAD_CELL_CONTENT: 'K-Chem-Periodic-Table-Head-Cell-Content', PERIODIC_TABLE_HEAD_CELL_GROUP: 'K-Chem-Periodic-Table-Head-Cell-Group', PERIODIC_TABLE_HEAD_CELL_PERIOD: 'K-Chem-Periodic-Table-Head-Cell-Period', ELEM_SYMBOL: 'K-Chem-Elem-Symbol', ELEM_SYMBOL_STUBS: 'K-Chem-Elem-Symbol-Stubs', ELEM_NAME: 'K-Chem-Elem-Name', ATOMIC_NUM: 'K-Chem-Atomic-Num', ATOMIC_WEIGHT: 'K-Chem-Atomic-Weight' }); Kekule.globalOptions.add('chemWidget.periodicTable',{ 'displayedComponents': ['symbol', 'name', 'atomicNumber', /*'atomicWeight',*/ 'groupHead', /*'periodHead',*/ 'legend'] }); /** * An widget to display periodic table and to select element on it. * @class * @augments Kekule.ChemWidget.AbstractWidget * * @param {Array} displayedComponents An array of string that decides which information of elements need to be shown in periodic table. * * @property {Array} displayedComponents An array of string that decides which information of elements need to be shown in periodic * table. The array may contains the following items: * ['symbol', 'name', 'atomicNumber', 'atomicWeight', 'groupHead', 'periodHead', 'legend'] * @property {Int} startingAtomNum * @property {Int} endingAtomNum * @property {String} startingElementSymbol * @property {String} endingElementSymbol * @property {Array} disabledElementSymbols Array of symbols that are unabled to be selected in table. * If property {@link Kekule.ChemWidget.PeriodicTable.enabledElementSymbols} is set, this property will be ignored. * @property {Array} enabledElementSymbols Array of symbols that are enabled to be selected in table. * @property {Bool} useMiniMode If true, table will be in small size and only show atom symbol/number information. * @property {Bool} enableSelect Whether user can interact with table and select element on it. * @property {Bool} enableMultiSelect Whether user can interact with table and select multiple elements on it. * @property {Hash} selected Selected element data. * @property {Array} selection Array of selected elements data (in multiselect mode). * @property {String} selectedSymbol Selected element symbol. * @property {Array} selectedSymbols Array of selected symbols (in multiselect mode). */ /** * Invoked when the an element is selected in periodic table. * event param of it has one fields: {elemData: Object} * @name Kekule.ChemWidget.PeriodicTable#select * @event */ /** * Invoked when the an element is deselected in periodic table. * event param of it has one fields: {elemData: Object} * @name Kekule.ChemWidget.PeriodicTable#deselect * @event */ Kekule.ChemWidget.PeriodicTable = Class.create(Kekule.ChemWidget.AbstractWidget, /** @lends Kekule.ChemWidget.PeriodicTable# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.PeriodicTable', /** @private */ BINDABLE_TAG_NAMES: ['span', 'div'], /** @private */ ELEM_DATA_FIELD: '__$elemData__', /** @private */ MAX_GROUP: 18, /** @private */ MAX_PERIOD: 7, /** @private */ LA_SERIES: ['La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu'], /** @private */ AC_SERIES: ['Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr'], /** @constructs */ initialize: function(/*$super, */parentOrElementOrDocument, displayedComponents) { this.setPropStoreFieldValue('displayedComponents', displayedComponents || this.getDefaultDisplayedComponents()); this._elemCells = []; // used internally this._selectedElemCells = []; // used internally this.tryApplySuper('initialize', [parentOrElementOrDocument]) /* $super(parentOrElementOrDocument) */; this.setEnableSelect(true); this.setEnableMultiSelect(true); }, /** @private */ initProperties: function() { this.defineProp('displayedComponents', {'dataType': DataType.ARRAY, 'getter': function() { return this.getPropStoreFieldValue('displayedComponents') || this.getDefaultDisplayedComponents(); } }); this.defineProp('startingAtomNum', {'dataType': DataType.INT}); this.defineProp('endingAtomNum', {'dataType': DataType.INT}); this.defineProp('startingElementSymbol', {'dataType': DataType.INT, 'serializable': false, 'getter': function() { var num = this.getStartingAtomNum(); return num && Kekule.ChemicalElementsDataUtil.getElementSymbol(num); }, 'setter': function(value) { this.setStartingAtomNum((value && Kekule.ChemicalElementsDataUtil.getAtomicNumber(value)) || null); } }); this.defineProp('endingElementSymbol', {'dataType': DataType.INT, 'serializable': false, 'getter': function() { var num = this.getEndingAtomNum(); return num && Kekule.ChemicalElementsDataUtil.getElementSymbol(num); }, 'setter': function(value) { this.setEndingAtomNum((value && Kekule.ChemicalElementsDataUtil.getAtomicNumber(value)) || null); } }); this.defineProp('enabledElementSymbols', {'dataType': DataType.ARRAY}); this.defineProp('disabledElementSymbols', {'dataType': DataType.ARRAY}); this.defineProp('useMiniMode', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('useMiniMode', value); if (value) this.addClassName(CCNS.PERIODIC_TABLE_MINI); else this.removeClassName(CCNS.PERIODIC_TABLE_MINI); } }); this.defineProp('enableSelect', {'dataType': DataType.BOOL, 'getter': function() { return this.getPropStoreFieldValue('enableSelect') || this.getEnableMultiSelect(); } }); this.defineProp('enableMultiSelect', {'dataType': DataType.BOOL}); this.defineProp('selected', {'dataType': DataType.HASH, 'serializable': false, 'setter': null, 'getter': function() { return this.getSelection()? this.getSelection()[0]: null; } }); this.defineProp('selection', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null, 'getter': function() { var result = []; var cellElems = this._selectedElemCells || []; for (var i = 0, l = cellElems.length; i < l; ++i) { var data = cellElems[i][this.ELEM_DATA_FIELD]; if (data) result.push(data); } return result; } }); this.defineProp('selectedSymbol', {'dataType': DataType.STRING, 'getter': function() { return this.getSelectedSymbols()[0]; }, 'setter': function(value) { this.setSelectedSymbols([value]); } }); this.defineProp('selectedSymbols', {'dataType': DataType.ARRAY, 'getter': function() { var selection = this.getSelection(); if (selection) { var result = []; for (var i = 0, l = selection.length; i < l; ++i) result.push(selection[i].symbol); return result; } return []; }, 'setter': function(value) { this.selectSymbols(value); } }); }, /** @ignore */ doGetWidgetClassName: function(/*$super*/) { return this.tryApplySuper('doGetWidgetClassName') /* $super() */ + ' ' + CCNS.PERIODIC_TABLE; }, /** @ignore */ doCreateRootElement: function(doc) { var result = doc.createElement('div'); return result; }, /** @ignore */ doCreateSubElements: function(/*$super, */doc, rootElem) { var result = this.tryApplySuper('doCreateSubElements', [doc, rootElem]) /* $super(doc, rootElem) */; var elem = this.createMainTable(doc, rootElem); return result.concat([elem]); }, /** @ignore */ doObjectChange: function(modifiedPropNames) { var props = ['displayedComponents', 'startingAtomNum', 'endingAtomNum']; //if (modifiedPropNames.indexOf('displayedComponents') >= 0) // need to recreate whole table if (Kekule.ArrayUtils.intersect(modifiedPropNames, props).length) { this.recreateMainTable(); } props = ['enabledElementSymbols', 'disabledElementSymbols']; if (Kekule.ArrayUtils.intersect(modifiedPropNames, props).length) { this.updateCellEnableStates(); } }, /** @private */ getDefaultDisplayedComponents: function() { //return ['symbol', 'name', 'atomicNumber', /*'atomicWeight',*/ 'groupHead', /*'periodHead',*/ 'legend']; return Kekule.globalOptions.chemWidget.periodicTable.displayedComponents; }, /** @private */ getShowElemSymbol: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('symbol') >= 0; }, /** @private */ getShowElemName: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('name') >= 0; }, /** @private */ getShowAtomicNum: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('atomicNumber') >= 0; }, /** @private */ getShowAtomicWeight: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('atomicWeight') >= 0; }, /** @private */ getShowGroupHead: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('groupHead') >= 0; }, /** @private */ getShowPeriodHead: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('periodHead') >= 0; }, /** @private */ getShowLegend: function() { var comps = this.getDisplayedComponents(); return comps.indexOf('legend') >= 0; }, /** * Clear old table and create a new one. * @param doc * @param parentElem * @private */ recreateMainTable: function() { this.deselectAll(); var elem = this.getElement(); DU.clearChildContent(elem); this.createMainTable(elem.ownerDocument, elem); }, /** * Create periodic main table (without La/Ac series). * @param {HTMLDocument} doc * @param {HTMLElement} parentElem * @returns {HTMLElement} * @private */ createMainTable: function(doc, parentElem) { var elemData = Kekule.chemicalElementsData; var cells = []; this._elemCells = []; var extraCells = []; var series = []; var result = doc.createElement('table'); // create all table rows and cells first (including head row) for (var row = 0; row <= this.MAX_PERIOD; ++row) { var rowElem = doc.createElement('tr'); var rowCells = []; for (var col = 0; col <= this.MAX_GROUP; ++col) { var cellElem = doc.createElement('td'); rowElem.appendChild(cellElem); rowCells.push(cellElem); } result.appendChild(rowElem); cells.push(rowCells); } // extra table for La/Ac series var extraTableElem = doc.createElement('table'); for (var row = 0; row < 2; ++row) { var rowElem = doc.createElement('tr'); var rowCells = []; for (var col = 0; col < this.LA_SERIES.length; ++col) { var cellElem = doc.createElement('td'); rowElem.appendChild(cellElem); rowCells.push(cellElem); } extraTableElem.appendChild(rowElem); extraCells.push(rowCells); } // then fill content var startingIndex = this.getStartingAtomNum() || 0; var endingIndex = this.getEndingAtomNum() || 300000; for (var i = 0, l = elemData.length; i < l; ++i) { var curr = elemData[i]; var atomNum = curr.atomicNumber; if (atomNum < startingIndex || atomNum > endingIndex) continue; var symbol = curr.symbol; var period = curr.period; var group = curr.group; var chemSerie = curr.chemicalSerie.replace(/\s/g, ''); Kekule.ArrayUtils.pushUnique(series, curr.chemicalSerie); var cellElem; var laIndex = this.LA_SERIES.indexOf(symbol); var acIndex = this.AC_SERIES.indexOf(symbol); if (laIndex >= 0) // Lanthanides { cellElem = extraCells[0][laIndex]; } else if (acIndex >= 0) // Actinides { cellElem = extraCells[1][acIndex]; } else if (period && group) // normal element { cellElem = cells[period][group]; } if (cellElem) { cellElem.className = CCNS.PERIODIC_TABLE_ELEM_CELL; // + ' ' + chemSerie.upperFirst(); cellElem.appendChild(this.createElemCellContent(doc, curr, chemSerie.upperFirst())); cellElem[this.ELEM_DATA_FIELD] = curr; this._elemCells.push(cellElem); } if ((laIndex === 0) || (acIndex === 0)) // create La/Ac stubs { cellElem = cells[period][group]; cellElem.className = CCNS.PERIODIC_TABLE_ELEM_STUBSCELL; cellElem.appendChild(this.createElemCellStubsContent(doc, curr, chemSerie.upperFirst())); } } //console.log(series); // fill head if (this.getShowGroupHead()) { var groupHeads = ['IA', 'IIA', 'IIIB', 'IVB', 'VB', 'VIB', 'VIIB', '', 'VIII', '', 'IB', 'IIB', 'IIIA', 'IVA', 'VA', 'VIA', 'VIIA', 'VIIIA']; for (var col = 1; col <= this.MAX_GROUP; ++col) { var row = (col === 1) || (col === 18)? 0: (col === 2) || (col >= 13)? 1: 3; var cellElem = cells[row][col]; cellElem.className = CCNS.PERIODIC_TABLE_HEAD_CELL + ' ' + CCNS.PERIODIC_TABLE_HEAD_CELL_GROUP; cellElem.appendChild(this.createHeadContent(doc, groupHeads[col - 1])); } } if (this.getShowPeriodHead()) { for (var row = 1; row <= this.MAX_PERIOD; ++row) { var cellElem = cells[row][0]; cellElem.className = CCNS.PERIODIC_TABLE_HEAD_CELL + ' ' + CCNS.PERIODIC_TABLE_HEAD_CELL_PERIOD; cellElem.appendChild(this.createHeadContent(doc, row)); } } parentElem.appendChild(result); // legend if (this.getShowLegend()) { var legendElem = doc.createElement('a'); legendElem.href="javascript:void(0)"; legendElem.className = CCNS.PERIODIC_TABLE_LEGEND + ' ' + CNS.CORNER_ALL; DU.setElementText(legendElem, /*CWT.LEGEND_CAPTION*/ Kekule.$L('ChemWidgetTexts.LEGEND_CAPTION')); var legendContentElem = doc.createElement('div'); legendContentElem.className = CCNS.PERIODIC_TABLE_LEGEND_CONTENT + ' ' + CNS.CORNER_ALL; // symbol legend var fakeInfo = { 'symbol': Kekule.$L('ChemWidgetTexts.LEGEND_ELEM_SYMBOL'), //CWT.LEGEND_ELEM_SYMBOL, 'atomicNumber': Kekule.$L('ChemWidgetTexts.LEGEND_ATOMIC_NUM'), //CWT.LEGEND_ATOMIC_NUM, 'naturalMass': Kekule.$L('ChemWidgetTexts.LEGEND_ATOMIC_WEIGHT'), //CWT.LEGEND_ATOMIC_WEIGHT, 'name': Kekule.$L('ChemWidgetTexts.LEGEND_ELEM_NAME') //CWT.LEGEND_ELEM_NAME } var symLegendElem = this.createElemCellContent(doc, fakeInfo, CCNS.PERIODIC_TABLE_LEGEND_ELEM_CELL_CONTENT); legendContentElem.appendChild(symLegendElem); // color legend var colorLegendElem = doc.createElement('div'); colorLegendElem.className = CCNS.PERIODIC_TABLE_LEGEND_COLORS; for (var i = 0, l = series.length; i < l; ++i) { var elem = doc.createElement('div'); elem.className = CCNS.PERIODIC_TABLE_LEGEND_COLOR + ' ' + series[i].replace(/\s/g, ''); DU.setElementText(elem, series[i]); colorLegendElem.appendChild(elem); } legendContentElem.appendChild(colorLegendElem); legendElem.appendChild(legendContentElem); parentElem.appendChild(legendElem); } parentElem.appendChild(extraTableElem); this.updateCellEnableStates(); return result; }, /** * Fill content in periodic table cell. * @private */ createElemCellContent: function(doc, elemInfo, extraClass) { var result = doc.createElement('div'); var className = CCNS.PERIODIC_TABLE_ELEM_CELL_CONTENT; if (extraClass) className += ' ' + extraClass; result.className = className; // symbol/name/atomic number/atomic weight if (this.getShowAtomicNum()) result.appendChild(this.createElemContentComponent(doc, elemInfo.atomicNumber, CCNS.ATOMIC_NUM)); if (this.getShowAtomicWeight()) { var mass = elemInfo.naturalMass; if (mass) { var smass = (typeof(mass) === 'number')? mass.toFixed(3): mass; result.appendChild(this.createElemContentComponent(doc, smass, CCNS.ATOMIC_WEIGHT)); } else // mass not set? { result.appendChild(this.createElemContentComponent(doc, '\u00a0', CCNS.ATOMIC_WEIGHT)); } } if (this.getShowElemSymbol()) result.appendChild(this.createElemContentComponent(doc, elemInfo.symbol, CCNS.ELEM_SYMBOL)); if (this.getShowElemName()) result.appendChild(this.createElemContentComponent(doc, elemInfo.name, CCNS.ELEM_NAME)); return result; }, /** * Create stub content in main table for Lanthanides and Actinides * @param doc * @param elemInfo * @private */ createElemCellStubsContent: function(doc, elemInfo, extraClass) { var result = doc.createElement('div'); result.className = CCNS.PERIODIC_TABLE_ELEM_CELL_CONTENT + ' ' + (extraClass || ''); var atomicNum, symbol, name; name = elemInfo.chemicalSerie; if (elemInfo.symbol === 'La') { atomicNum = '57-71'; symbol = 'La-Lu'; } else { atomicNum = '89-103'; symbol = 'Ac-Lr'; } // symbol/name/atomic number/atomic weight if (this.getShowAtomicNum()) result.appendChild(this.createElemContentComponent(doc, atomicNum, CCNS.ATOMIC_NUM)); if (this.getShowElemSymbol()) result.appendChild(this.createElemContentComponent(doc, symbol, CCNS.ELEM_SYMBOL_STUBS, true)); if (this.getShowElemName()) result.appendChild(this.createElemContentComponent(doc, name, CCNS.ELEM_NAME)); return result; }, /** @private */ createElemContentComponent: function(doc, value, className, wrapSpan) { var elem = doc.createElement('span'); elem.className = className; if (wrapSpan) { var wrapper = doc.createElement('span'); elem.appendChild(wrapper); } DU.setElementText(wrapper || elem, value); return elem; }, /** @private */ createHeadContent: function(doc, text, extraClass) { var result = doc.createElement('div'); result.className = CCNS.PERIODIC_TABLE_HEAD_CELL_CONTENT + ' ' + (extraClass || ''); DU.setElementText(result, text); return result; }, /** @private */ updateCellEnableStates: function() { var enabledSymbols = this.getEnabledElementSymbols() || []; var disabledSymbols = (!enabledSymbols.length && this.getDisabledElementSymbols()) || []; if (!disabledSymbols.length && !enabledSymbols.length) return; // if enabled symbols is set, disabled symbols will be ignored var cells = this._elemCells; for (var i = 0, l = cells.length; i < l; ++i) { var data = this._getCellData(cells[i]); var currSymbol = data.symbol; if (enabledSymbols.length) { if (enabledSymbols.indexOf(currSymbol) < 0) // disable current cell { this.setCellEnableState(cells[i], false); } } else if (disabledSymbols.length) { if (disabledSymbols.indexOf(currSymbol) >= 0) // disable current cell { this.setCellEnableState(cells[i], false); } } } }, /** @private */ setCellEnableState: function(cellElem, enabled) { if (enabled) { EU.removeClass(cellElem, CNS.STATE_DISABLED); this._getCellData(cellElem).disabled = null; } else { EU.addClass(cellElem, CNS.STATE_DISABLED); this._getCellData(cellElem).disabled = true; } }, // interaction methods and event handlers /** @private */ _getCellData: function(cellElem) { return cellElem[this.ELEM_DATA_FIELD]; }, /** @private */ _isCellDisabled: function(cellElem) { return !!this._getCellData(cellElem).disabled; }, /** @private */ _getAtomCellElem: function(elem) { var cellElem = DU.getNearestAncestorByTagName(elem, 'td', true); //if (cellElem && EU.hasClass(cellElem, CCNS.PERIODIC_TABLE_ELEM_CELL)) if (cellElem && (this._elemCells.indexOf(cellElem) >= 0)) return cellElem; else return null; }, /** @private */ _getCellElemOfSymbol: function(symbol) { var cells = this._elemCells; for (var i = 0, l = cells.length; i < l; ++i) { var cell = cells[i]; var data = cell[this.ELEM_DATA_FIELD]; if (data.symbol === symbol) return cell; } return null; }, /** @private */ isCellSelected: function(cellElem) { return this._selectedElemCells.indexOf(cellElem) >= 0; }, /** @private */ selectCell: function(cellElem) { if (cellElem) { EU.addClass(cellElem, CNS.STATE_SELECTED); Kekule.ArrayUtils.pushUnique(this._selectedElemCells, cellElem); this.invokeEvent('select', {'elemData': cellElem[this.ELEM_DATA_FIELD]}); } }, /** @private */ deselectCell: function(cellElem) { if (cellElem) { EU.removeClass(cellElem, CNS.STATE_SELECTED); Kekule.ArrayUtils.remove(this._selectedElemCells, cellElem); this.invokeEvent('deselect', {'elemData': cellElem[this.ELEM_DATA_FIELD]}); } }, /** @private */ toggleCell: function(cellElem) { if (this.isCellSelected(cellElem)) this.deselectCell(cellElem); else this.selectCell(cellElem); }, /** * Deselect all elements in table. */ deselectAll: function() { var cells = this._selectedElemCells; for (var i = cells.length - 1; i >= 0; --i) this.deselectCell(cells[i]); return this; }, /** * Select elements of symbols in table. * @param {Array} symbols */ selectSymbols: function(symbols) { this.deselectAll(); var ss = Kekule.ArrayUtils.toArray(symbols); for (var i = 0, l = ss.length; i < l; ++i) { var cell = this._getCellElemOfSymbol(ss[i]); if (cell) this.selectCell(cell); } return this; }, /** @ignore */ react_click: function(/*$super, */e) { if (this.getEnableSelect()) { var target = e.getTarget(); var cellElem = this._getAtomCellElem(target); if (cellElem && !this._isCellDisabled(cellElem)) { if (!this.getEnableMultiSelect()) { this.deselectAll(); this.selectCell(cellElem); } else { this.toggleCell(cellElem); } return true; } } this.tryApplySuper('react_click', [e]) /* $super(e) */; } }); })();