/**
* @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 {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
$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('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 $super() + ' ' + CCNS.PERIODIC_TABLE;
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('div');
return result;
},
/** @ignore */
doCreateSubElements: function($super, doc, rootElem)
{
$super(doc, rootElem);
var elem = this.createMainTable(doc, rootElem);
return [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();
}
},
/** @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);
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;
},
// interaction methods and event handlers
/** @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)
{
if (!this.getEnableMultiSelect())
{
this.deselectAll();
this.selectCell(cellElem);
}
else
{
this.toggleCell(cellElem);
}
return true;
}
}
$super(e);
}
});
})();