itsa-react-table
Version:
Editable React.js table
1,245 lines (1,197 loc) • 50.8 kB
JSX
'use strict';
/**
* Description here
*
*
*
* <i>Copyright (c) 2016 ItsAsbreuk - http://itsasbreuk.nl</i><br>
* New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
*
*
* @module component.jsx
* @class Component
* @since 2.0.0
*/
require('itsa-jsext');
require('itsa-dom');
const React = require('react'),
PropTypes = require('prop-types'),
utils = require('itsa-utils'),
async = utils.async,
later = utils.later,
Button = require('itsa-react-button'),
serializeStyles = require('./serialize-styles'),
CLICK = 'click',
RESIZE = 'resize',
MAIN_CLASS = 'itsa-table',
ROW_CLASS = 'itsa-table-row',
CELL_CLASS = 'itsa-table-cell itsa-table-col-',
EDITABLE_CELL_CLASS_SPACED = ' itsa-table-editable-cell',
ROW_REMOVE_CLASS = '__row-remove',
ROW_ADD_CLASS = '__row-add',
REGEXP_TRANSPARENT = /^rgba\(\d+\,( )?\d+\,( )?\d+\,( )?0\)$/,
COPY_STYLES = [
'width',
'height',
'padding-left',
'padding-right',
'padding-top',
'padding-bottom',
'border-top-color',
'border-top-left-radius',
'border-top-right-radius',
'border-top-style',
'border-top-width',
'border-bottom-color',
'border-bottom-left-radius',
'border-bottom-right-radius',
'border-bottom-style',
'border-bottom-width',
'border-left-color',
'border-left-style',
'border-left-width',
'border-right-color',
'border-right-style',
'border-right-width',
'background-color',
'background-image',
'background-attachment',
'background-blend-mode',
'background-clip',
'background-origin',
'background-position-x',
'background-position-y',
'background-repeat-x',
'background-repeat-y',
'background-size',
'color',
'font-family',
'font-feature-settings',
'font-kerning',
'font-size',
'font-stretch',
'font-style',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-variant-settings',
'font-weight',
'font-smooting',
'font-size-delta',
'opacity',
'overflow',
'white-space',
'visibility',
'-webkit-font-smooting',
'-webkit-font-size-delta',
'-ms-font-smooting',
'-ms-font-size-delta',
'text-align',
'text-align-last',
'text-combine-upright',
'text-decoration-color',
'text-decoration-line',
'text-decoration-skip-ink',
'text-decoration-style',
'text-indent',
'text-orientation',
'text-overflow',
'text-rendering',
'text-shadow',
'text-size-adjust',
'text-transform',
'text-underline-position',
'-webkit-text-align',
'-webkit-text-combine',
'-webkit-text-decorations-in-effect',
'-webkit-text-emphasis-color',
'-webkit-text-emphasis-position',
'-webkit-text-emphasis-style',
'-webkit-text-fill-color',
'-webkit-text-orientation',
'-webkit-text-security',
'-webkit-text-stroke-color',
'-webkit-text-stroke-width',
'-ms-text-align',
'-ms-text-combine',
'-ms-text-decorations-in-effect',
'-ms-text-emphasis-color',
'-ms-text-emphasis-position',
'-ms-text-emphasis-style',
'-ms-text-fill-color',
'-ms-text-orientation',
'-ms-text-security',
'-ms-text-stroke-color',
'-ms-text-stroke-width'
];
const retrieveFieldName = field => {
return (typeof field==='object') ? field.key : field;
};
const cloneData = arr => {
return arr.map(record => {
let newRecord = {};
record.itsa_each((value, key) => {
newRecord[key] = value;
});
return newRecord;
});
};
class Table extends React.Component {
constructor(props) {
super(props);
const instance = this;
instance.state = {
editableRow: null,
editableCol: null,
editValue: ''
};
instance.changeCell = instance.changeCell.bind(instance);
instance.handleCellKeyDown = instance.handleCellKeyDown.bind(instance);
instance.focus = instance.focus.bind(instance);
instance._focusActiveCell = instance._focusActiveCell.bind(instance);
instance.generateHead = instance.generateHead.bind(instance);
instance.generateRows = instance.generateRows.bind(instance);
instance.refocus = instance.refocus.bind(instance);
instance.addRow = instance.addRow.bind(instance);
instance.addCol = instance.addCol.bind(instance);
instance.deleteRow = instance.deleteRow.bind(instance);
instance._handleDocumentClick = instance._handleDocumentClick.bind(instance);
instance._handleResize = instance._handleResize.bind(instance);
instance.focusTextArea = instance.focusTextArea.bind(instance);
instance.setFixedHeaderDimensions = instance.setFixedHeaderDimensions.bind(instance);
}
componentDidMount() {
const instance = this,
props = instance.props;
instance._tableWidth = instance._tableNode.offsetWidth;
// set outside clickHandler which watches for outside clicks that will collapse the component:
instance.IE8_EVENTS = !instance._componentNode.addEventListener;
if (instance.IE8_EVENTS) {
document.attachEvent('on'+CLICK, instance._handleDocumentClick);
window.attachEvent('on'+RESIZE, instance._handleResize);
}
else {
document.addEventListener(CLICK, instance._handleDocumentClick, true);
window.addEventListener(RESIZE, instance._handleResize, true);
}
props.autoFocus && instance.focus();
if (props.fixedHeaders) {
async(instance.setFixedHeaderDimensions);
instance._timer = later(instance.setFixedHeaderDimensions, 300, true);
}
}
/**
* componentWilUnmount does some cleanup.
*
* @method componentWillUnmount
* @since 0.0.1
*/
componentWillUnmount() {
const instance = this;
instance._timer && instance._timer.cancel();
instance.unmounted = true;
if (instance.IE8_EVENTS) {
document.detachEvent('on'+CLICK, instance._handleDocumentClick);
window.detachEvent('on'+RESIZE, instance._handleResize);
}
else {
document.removeEventListener(CLICK, instance._handleDocumentClick, true);
window.removeEventListener(RESIZE, instance._handleResize, true);
}
}
componentDidUpdate() {
this.setFixedHeaderDimensions();
}
_handleResize() {
const instance = this,
newWidth = instance._tableNode.offsetWidth;
if (instance._tableWidth!==newWidth) {
instance._tableWidth = newWidth;
instance.setFixedHeaderDimensions();
}
}
setFixedHeaderDimensions() {
let headNode, ths;
const instance = this,
props = instance.props;
if (props.fixedHeaders && props.columns && !instance.unmounted) {
headNode = instance._headNode;
if (headNode) {
ths = headNode.itsa_getAll('th');
Array.prototype.forEach.call(ths, thNode => {
let inlineStyle = {},
fixedNode, fixedContainerNode, contStyle, currentLeft, currentTop, prevAttr, newAttr;
COPY_STYLES.forEach(style => {
let nodeStyle = thNode.itsa_getStyle(style);
if ((style==='background-color') && nodeStyle && REGEXP_TRANSPARENT.test(nodeStyle)) {
nodeStyle = undefined; // fixed headers cannot be transparent -> revert to the class background-color
}
(nodeStyle===undefined) || (inlineStyle[style]=nodeStyle+' !important');
});
fixedContainerNode = thNode.itsa_getElement('div.itsa-table-header-cont');
if (fixedContainerNode) {
currentLeft = fixedContainerNode.itsa_getInlineStyle('left');
if (currentLeft) {
currentLeft = parseInt(currentLeft, 10);
}
else {
currentLeft = 0;
}
currentTop = fixedContainerNode.itsa_getInlineStyle('top');
if (currentTop) {
currentTop = parseInt(currentTop, 10);
}
else {
currentTop = 0;
}
contStyle = {
left: (thNode.itsa_left-fixedContainerNode.itsa_left+currentLeft)+'px',
top: (thNode.itsa_top-fixedContainerNode.itsa_top+currentTop)+'px'
};
prevAttr = fixedContainerNode.getAttribute('style');
newAttr = serializeStyles.serialize(contStyle);
(prevAttr===newAttr) || fixedContainerNode.setAttribute('style', newAttr);
}
fixedNode = thNode.itsa_getElement('div.itsa-table-header');
if (fixedNode) {
prevAttr = fixedNode.getAttribute('style');
newAttr = serializeStyles.serialize(inlineStyle);
(prevAttr===newAttr) || fixedNode.setAttribute('style', newAttr);
}
});
}
}
}
changeCell(e) {
const value = e.target.value;
this._editValueBeforeBlur = value;
this.setState({
editValue: value
});
}
handleCellKeyDown(e) {
const instance = this;
if (e.keyCode===27) {
instance.setState(prevState => {
return {
editValue: instance._editValueBeforeEdit,
editableRow: null,
editableCol: null
};
}, () => {
instance._blurActiveCell();
});
}
}
implementCellChanges(rowIndex, field, force) {
let changed, newData, col, secondField, x, y, columns, cells, editableCols;
const instance = this,
props = instance.props,
propsData = props.data,
state = instance.state,
onChange = props.onChange,
onChangeCell = props.onChangeCell,
selectedRange = state.selectedRange,
editValueBeforeBlur = instance._editValueBeforeBlur,
editValueBeforeEdit = instance._editValueBeforeEdit;
delete instance._editValueBeforeBlur;
if (typeof field==='object') {
field = field.key;
}
if (((editValueBeforeEdit===editValueBeforeBlur) && !selectedRange) || (editValueBeforeBlur===undefined)) {
// nothing changed
return;
}
// if ((props.data[rowIndex][field]==editValueBeforeBlur) ||
// ((props.data[rowIndex][field]===undefined) && (editValueBeforeBlur==='')) ||
// ((props.data[rowIndex][field]===null) && (editValueBeforeBlur===''))) { // DO NOT tripple check -> the original value may not be a string, whereas the editvalue is!!
// // nothing changed
// return;
// }
if (selectedRange) {
// editableCols: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
if (force!==true) {
return;
}
columns = props.columns;
if (!columns || (columns.length===0)) {
columns = instance._columns;
}
editableCols = props.editableCols;
if (typeof editableCols==='number') {
editableCols = [editableCols];
}
}
if (onChange) {
changed = false;
newData = cloneData(propsData);
if (newData[rowIndex][field]!=editValueBeforeBlur) {
changed = true;
newData[rowIndex][field] = editValueBeforeBlur;
}
// we might need to change multiple cells, in case `multiEdit` is set, which leads into a value for state.selectedRange:
if (selectedRange) {
// editableCols: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
for (x=selectedRange.x1; x<=selectedRange.x2; x++) {
if (!editableCols || editableCols.itsa_contains(x)) {
for (y=selectedRange.y1; y<=selectedRange.y2; y++) {
col = columns[x];
secondField = (typeof col==='string') ? col : col.key;
if (newData[y][secondField]!=editValueBeforeBlur) {
changed = true;
newData[y][secondField] = editValueBeforeBlur;
}
}
}
}
}
changed && onChange(newData);
}
if (onChangeCell) {
if (!props.multiEdit) {
if (propsData[rowIndex][field]!=editValueBeforeBlur) {
onChangeCell(rowIndex, field, editValueBeforeBlur);
}
}
else {
changed = false;
cells = [{row: rowIndex, field}];
if (propsData[rowIndex][field]!=editValueBeforeBlur) {
changed = true;
}
// we might need to change multiple cells, in case `multiEdit` is set, which leads into a value for state.selectedRange:
if (selectedRange) {
// editableCols: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
for (x=selectedRange.x1; x<=selectedRange.x2; x++) {
if (!editableCols || editableCols.itsa_contains(x)) {
for (y=selectedRange.y1; y<=selectedRange.y2; y++) {
col = columns[x];
secondField = (typeof col==='string') ? col : col.key;
cells.push({row: y, field: secondField});
if (propsData[y][secondField]!=editValueBeforeBlur) {
changed = true;
}
}
}
}
}
changed && onChangeCell(cells, editValueBeforeBlur);
}
}
if (selectedRange) {
instance.setState({selectedRange: null});
}
}
focus() {
let editableCol, columns, hasColumns, item, editValue, editableCols;
const instance = this,
props = instance.props,
state = instance.state;
editableCols = props.editableCols;
if ((state.editableRow===null) || (state.editableCol===null)) {
editableCol = props.rowHeader ? 1 : 0;
if (typeof editableCols==='number') {
editableCols = [editableCols];
}
if (editableCols) {
editableCol += editableCols[0];
if (props.rowHeader) {
editableCol--;
}
}
columns = props.columns;
hasColumns = (columns && (columns.length>0));
item = props.data[0];
editValue = hasColumns ? item[retrieveFieldName(columns[editableCol])] : item[retrieveFieldName(instance._columns[editableCol])];
instance.setState({
editableRow: 0,
editableCol,
editValue,
selectedRangeStart: {
x: editableCol,
y: 0
}
});
}
instance._focusActiveCell();
}
focusTextArea(e) {
let length;
const instance = this,
node = e.target;
instance._editValueBeforeBlur = node.value;
if (instance.props.fullSelectOnEdit) {
length = node.value.length;
node.setSelectionRange(length, length);
}
}
_blurActiveCell() {
const instance = this,
state = instance.state,
textareaNode = instance['_textarea_'+state.editableRow+'_'+state.editableCol],
componentContainerNode = instance['_component_'+state.editableRow+'_'+state.editableCol],
componentNode = componentContainerNode && componentContainerNode.itsa_getElement('button'),
focussableNode = textareaNode || componentNode;
if (focussableNode && ((document.activeElement===focussableNode) || document.activeElement.contains(focussableNode))) {
focussableNode.blur();
}
}
_focusActiveCell() {
const instance = this;
async(() => {
let length;
const state = instance.state,
textareaNode = instance['_textarea_'+state.editableRow+'_'+state.editableCol],
componentContainerNode = instance['_component_'+state.editableRow+'_'+state.editableCol],
componentNode = componentContainerNode && componentContainerNode.itsa_getElement('button');
if (textareaNode && (document.activeElement!==textareaNode)) {
instance._editValueBeforeEdit = textareaNode.value || '';
textareaNode.focus();
if (textareaNode.setSelectionRange) {
length = textareaNode.value.length;
textareaNode.setSelectionRange(0, length);
}
}
else if (componentNode) {
componentNode.focus();
}
});
}
scrollTo(amount) {
this._componentNode.scrollTop = amount;
}
generateHead() {
let cols, alreadyDefined,
j = -1;
const instance = this,
props = instance.props,
removeableY = props.removeableY,
extendableY = props.extendableY,
columns = props.columns,
fixedHeaders = props.fixedHeaders,
rowHeader = props.rowHeader,
onHeaderClick = props.onHeaderClick;
if (columns && (columns.length>0)) {
// first dedupe duplicated col-keys
alreadyDefined = {};
cols = columns.filter(col => {
let dupe;
const field = (typeof col==='string') ? col : col.key;
dupe = alreadyDefined[field];
alreadyDefined[field] = true;
return !dupe;
}).map((col, i) => {
let colName, classname, key, cellContent, headerClick;
const field = (typeof col==='string') ? col : col.key;
classname = 'itsa-table-header itsa-table-col-'+field;
headerClick = e => {
let selectedRange;
const state = instance.state,
rowIndex = state.editableRow,
colIndex = state.editableCol,
editCol = (typeof colIndex==='number') && columns[colIndex],
editField = editCol && ((typeof editCol==='string') ? editCol : editCol.key);
if (state.selectedRange) {
selectedRange = state.selectedRange.itsa_deepClone();
}
instance.setState({
editableRow: null,
editableCol: null,
editValue: ''
}, () => {
if (selectedRange) {
instance.setState({selectedRange: null});
}
});
if (typeof rowIndex==='number') {
instance.implementCellChanges(rowIndex, editField);
}
if (onHeaderClick) {
async(onHeaderClick.call(null, field, editField, state.editValue, rowIndex, colIndex, selectedRange, e));
}
};
if ((i>0) || !rowHeader) {
colName = (typeof col==='string') ? col : (col.label || col.key);
key = (typeof col==='string') ? col : col.key;
}
else {
classname += ' itsa-table-header-rowheader';
key=j--;
}
if (fixedHeaders) {
colName || (colName=' ');
cellContent = '<div class="itsa-table-header-cont"><div class="itsa-table-header">'+colName+'</div></div>'+colName;
return (<th className={classname} dangerouslySetInnerHTML={{__html: cellContent}} key={key} onClick={headerClick} />);
}
return (<th className={classname} dangerouslySetInnerHTML={{__html: colName}} key={key} onClick={headerClick} />);
});
if (extendableY==="full") {
if (fixedHeaders) {
cols.unshift((<th className={'itsa-table-header '+ROW_ADD_CLASS} key={j--}><div className="itsa-table-header-cont"><div className="itsa-table-header"> </div></div> </th>));
}
else {
cols.unshift((<th className={'itsa-table-header '+ROW_ADD_CLASS} key={j--} />));
}
}
if (removeableY) {
if (fixedHeaders) {
cols.unshift((<th className={'itsa-table-header '+ROW_REMOVE_CLASS} key={j--}><div className="itsa-table-header-cont"><div className="itsa-table-header"> </div></div> </th>));
}
else {
cols.unshift((<th className={'itsa-table-header '+ROW_REMOVE_CLASS} key={j--} />));
}
}
return (
<thead ref={node => instance._headNode=node}>
<tr>
{cols}
</tr>
</thead>
);
}
}
generateRows() {
let rowclass, editableColsArray;
const instance = this,
props = instance.props,
state = instance.state,
data = props.data,
disabled = props.disabled,
columns = props.columns,
selectedRange = state.selectedRange,
editableCols = props.editableCols,
rowClassRenderer = props.rowClassRenderer,
rowHeader = props.rowHeader,
editable = props.editable,
removeableY = props.removeableY,
extendableY = props.extendableY,
fullEditable = (editable==='full'),
hasColumns = (columns && (columns.length>0));
const colIsEditable = colIndex => {
if (disabled) {
return false;
}
if (!editableCols) {
return true;
}
if (typeof editableCols==='number') {
return editableCols===colIndex;
}
return editableCols.itsa_contains(colIndex);
};
if (editableCols) {
editableColsArray = (typeof editableCols==='number') ? [editableCols] : editableCols;
}
return data.map((rowdata, i) => {
let cells, extraClass;
if (hasColumns) {
// create based upon the columns
cells = columns.map((col, j) => {
const field = (typeof col==='string') ? col : col.key;
let classname = CELL_CLASS+field,
value = rowdata[field],
cellContent, onBlur, textAreaValue;
if (rowHeader && (j===0)) {
classname += ' itsa-table-rowheader';
if (value===null) {
value = '';
}
cellContent = String(value);
return (
<td
className={classname}
dangerouslySetInnerHTML={{__html: cellContent}}
data-colid={j}
key={field} />
);
}
else if (Object.itsa_isObject(value)) {
return (
<td
className={classname}
data-colid={j}
key={field}
ref={node => instance['_component_'+i+'_'+j] = node}>
{value}
</td>
);
}
else if (fullEditable || ((editable===true) && (state.editableRow===i) && (state.editableCol===j)) && colIsEditable(j)) {
classname += EDITABLE_CELL_CLASS_SPACED;
(typeof value==='number') || value || (value='');
value = String(value);
if ((state.editableRow===i) && (state.editableCol===j) && (typeof instance._editValueBeforeBlur==='string')) {
textAreaValue = state.editValue;
}
else {
textAreaValue = value;
}
if ((textAreaValue===null) || (textAreaValue===undefined)) {
textAreaValue = '';
}
fullEditable && (onBlur=instance.implementCellChanges.bind(instance, i, field));
cellContent = (
<textarea
disabled={disabled}
onBlur={onBlur}
onChange={instance.changeCell}
onFocus={instance.focusTextArea}
onKeyDown={instance.handleCellKeyDown}
ref={node => instance['_textarea_'+i+'_'+j] = node}
rows={1}
value={textAreaValue} />
);
return (
<td
className={classname}
data-colid={j}
key={field}>
<span>{textAreaValue}</span>
{cellContent}
</td>
);
}
(typeof value==='number') || value || (value='');
// we may need to add an 'selected' class:
if (selectedRange &&
(j>=selectedRange.x1) &&
(j<=selectedRange.x2) &&
(i>=selectedRange.y1) &&
(i<=selectedRange.y2) &&
(!editableCols || editableColsArray.itsa_contains(j))) {
classname += ' selected-range';
}
if (typeof value!=='object') {
value = String(value);
(value.itsa_trim()==='') && (value=' ');
cellContent = value.itsa_replaceAll('\n', '<br />');
return (
<td
className={classname}
dangerouslySetInnerHTML={{__html: cellContent}}
data-colid={j}
key={field} />
);
}
// else
return (
<td
className={classname}
data-colid={j}
key={field}
ref={node => instance['_component_'+i+'_'+j] = node}>
{value}
</td>
);
});
}
else {
// all fields
cells = [];
let j = -1;
instance._columns = [];
rowdata.itsa_each((value, key) => {
const field = (typeof key==='string') ? key : key.key;
let classname = CELL_CLASS+key,
cellContent, onBlur, textAreaValue,
colCount = cells.length;
j++;
instance._columns[j] = key;
if (rowHeader && (colCount===0)) {
classname += ' itsa-table-rowheader';
if (value===null) {
value = '';
}
cellContent = value;
cells.push((<td className={classname} dangerouslySetInnerHTML={{__html: cellContent}} data-colid={colCount} key={key} />));
}
else if (Object.itsa_isObject(value)) {
return (
<td
className={classname}
data-colid={j}
key={field}
ref={node => instance['_component_'+i+'_'+j] = node}>
{value}
</td>
);
}
else if (fullEditable || ((editable===true) && (state.editableRow===i) && (state.editableCol===colCount))) {
classname += EDITABLE_CELL_CLASS_SPACED;
(typeof value==='number') || value || (value='');
value = String(value);
if ((state.editableRow===i) && (state.editableCol===j)) {
textAreaValue = state.editValue;
}
else {
textAreaValue = value;
}
if ((textAreaValue===null) || (textAreaValue===undefined)) {
textAreaValue = '';
}
fullEditable && (onBlur=instance.implementCellChanges.bind(instance, i, field));
cellContent = (
<textarea
disabled={disabled}
onBlur={onBlur}
onChange={instance.changeCell}
onFocus={instance.focusTextArea}
onKeyDown={instance.handleCellKeyDown}
ref={node => instance['_textarea_'+i+'_'+colCount] = node}
rows={1}
value={textAreaValue} />
);
cells.push((
<td className={classname} data-colid={colCount} key={key}>
{cellContent}
<span>{textAreaValue}</span>
</td>
));
}
else {
(typeof value==='number') || value || (value='');
// we may need to add an 'selected' class:
if (selectedRange &&
(j>=selectedRange.x1) &&
(j<=selectedRange.x2) &&
(i>=selectedRange.y1) &&
(i<=selectedRange.y2) &&
(!editableCols || editableColsArray.itsa_contains(j))) {
classname += ' selected-range';
}
if (typeof value!=='object') {
value = String(value);
(value.itsa_trim()==='') && (value=' ');
cellContent = value.itsa_replaceAll('\n', '<br />');
cells.push((<td className={classname} dangerouslySetInnerHTML={{__html: cellContent}} data-colid={colCount} key={key} />));
}
else {
cells.push((<td className={classname} data-colid={colCount} key={key} ref={node => instance['_component_'+i+'_'+j] = node}>{value}</td>));
}
}
});
}
if (extendableY==='full') {
cells.unshift((
<td className={CELL_CLASS+ROW_ADD_CLASS} key={ROW_ADD_CLASS}>
<Button buttonText="+" className="controll-btn" disabled={disabled} onClick={instance.addRow.bind(instance, i)} />
</td>
));
}
if (removeableY) {
cells.unshift((
<td className={CELL_CLASS+ROW_REMOVE_CLASS} key={ROW_REMOVE_CLASS}>
<Button buttonText="-" className="controll-btn" disabled={disabled} onClick={instance.deleteRow.bind(instance, i)} />
</td>
));
}
rowclass = ROW_CLASS;
if (rowClassRenderer) {
extraClass = rowClassRenderer(i, rowdata);
extraClass && (rowclass+=' '+extraClass);
}
return (<tr className={rowclass} data-recordid={i} data-rowid={i} key={i}>{cells}</tr>);
});
}
refocus(e) {
let focusRow, focusCol, match, maxRow, maxCol, firstItem, item, editValue, colChangedByRow, isSelectComponent, less,
prevRowIndex, prevColIndex, field, editDirectionDown, arrowDown, arrowUp, node, trNode, trParentNode, tds, trs;
const instance = this,
props = instance.props,
state = instance.state,
keyCode = e.keyCode,
shiftKey = e.shiftKey,
ctrlKey = e.metaKey || e.ctrlKey,
data = props.data,
loop = props.loop,
editableCols = props.editableCols,
cursorNav = props.cursorNav,
lowestColIndex = props.rowHeader ? 1 : 0,
highestColIndex = data.itsa_keys().length-(props.rowHeader ? 0 : 1),
columns = props.columns,
hasColumns = (columns && (columns.length>0));
const implementChanges = keepFocus => {
if ((props.editable===true) || (state.selectedRange)) { // NOT 'full' for that would take care of itself
field = hasColumns ? columns[prevColIndex] : instance._columns[prevColIndex];
instance.implementCellChanges(prevRowIndex, field, true);
keepFocus && (instance._editValueBeforeEdit=instance._editValueBeforeBlur);
}
};
editDirectionDown = ((keyCode===9) || (keyCode===37) || (keyCode===39)) ? false : props.editDirectionDown;
if (keyCode===13) {
if (shiftKey) {
return;
}
}
match = (
(keyCode===9) ||
(keyCode===13) ||
(cursorNav && ((keyCode===40) || (keyCode===38))) ||
(cursorNav && ctrlKey && ((keyCode===37) || (keyCode===39)))
); // 40=arrowDown, 38=arrowUp
// we need to ignore MOVING DOWN in case the focus lies on a button component
if ((document.activeElement.tagName==='BUTTON') && (keyCode===13)) {
match = false;
}
isSelectComponent = (document.activeElement.itsa_hasClass('itsa-select') || document.activeElement.itsa_inside('.itsa-select'));
if (isSelectComponent && ((keyCode===13) || (cursorNav && ((keyCode===38) || (keyCode===40))))) {
match = false;
}
if (match) {
e.preventDefault();
arrowDown = (keyCode===40);
arrowUp = (keyCode===38);
maxRow = data.length - 1;
if (columns) {
maxCol = columns.length - 1;
}
else {
firstItem = data[0];
maxCol = firstItem ? firstItem.itsa_size()-1 : 0;
}
if (isSelectComponent) {
// we need to catch editableRow and editableCol because thay are not set
less = 0;
if (props.removeableY) {
less++;
}
if (props.extendableY==='full') {
less++;
}
node = document.activeElement;
while (node && (node.tagName!=='TD')) {
node = node.parentNode;
}
trNode = node.parentNode;
tds = trNode.childNodes;
prevColIndex = Array.prototype.indexOf.call(tds, node) - less;
trParentNode = trNode.parentNode;
trs = trParentNode.childNodes;
prevRowIndex = Array.prototype.indexOf.call(trs, trNode);
}
else {
prevRowIndex = state.editableRow;
prevColIndex = state.editableCol;
}
focusRow = prevRowIndex;
focusCol = prevColIndex;
if (((keyCode===9) && shiftKey) || ((keyCode===37) && ctrlKey) || arrowUp) {
// backwards
if (editDirectionDown || arrowUp) {
focusRow--;
}
else {
do {
focusCol--;
if (focusCol<lowestColIndex) {
break;
}
}
while (!editableCols || !editableCols.itsa_contains(focusCol));
}
}
else {
// forewards
if (editDirectionDown || arrowDown) {
focusRow++;
}
else {
do {
focusCol++;
if (focusCol>highestColIndex) {
break;
}
}
while (!editableCols || !editableCols.itsa_contains(focusCol));
}
}
// now we might need to adjust the values when out of range
if (focusRow<0) {
if (!loop) {
implementChanges(true);
return;
}
focusRow = maxRow;
do {
focusCol--;
if (focusCol<lowestColIndex) {
focusCol = highestColIndex;
}
}
while (!editableCols || !editableCols.itsa_contains(focusCol));
colChangedByRow = true;
}
else if (focusRow>maxRow) {
if (!loop) {
implementChanges(true);
return;
}
focusRow = 0;
do {
focusCol++;
if (focusCol>highestColIndex) {
focusCol = lowestColIndex;
}
}
while (!editableCols || !editableCols.itsa_contains(focusCol));
colChangedByRow = true;
}
if (focusCol<lowestColIndex) {
if (!loop) {
implementChanges(true);
return;
}
colChangedByRow || focusRow--;
(focusRow<0) && (focusRow=maxRow);
focusCol = maxCol;
}
else if (focusCol>maxCol) {
if (!loop) {
implementChanges(true);
return;
}
colChangedByRow || focusRow++;
focusCol = lowestColIndex;
(focusRow>maxRow) && (focusRow=0);
}
item = props.data[focusRow];
if (item) {
editValue = hasColumns ? item[retrieveFieldName(columns[focusCol])] : item[retrieveFieldName(instance._columns[focusCol])];
this.setState({
editableRow: focusRow,
editableCol: focusCol,
editValue,
selectedRangeStart: {
x: focusCol,
y: focusRow
},
selectedRange: null
});
instance._focusActiveCell();
implementChanges();
}
}
}
handleClick(editable, e) {
const instance = this,
props = instance.props,
onClick = instance.props.onClick,
state = instance.state,
shiftClick = e.shiftKey;
let node = e.target,
editableCols = props.editableCols,
colId, rowId, columns, hasColumns, item, editValue, newState, prevRowId, prevColId, selectedRangeStart;
(node.tagName==='TD') || (node=node.parentNode);
colId = parseInt(node.getAttribute('data-colid'), 10);
node = node.parentNode;
rowId = parseInt(node.getAttribute('data-rowid'), 10);
if (typeof editableCols==='number') {
editableCols = [editableCols];
}
if (editable && (!editableCols || editableCols.itsa_contains(colId))) {
columns = props.columns;
hasColumns = (columns && (columns.length>0));
item = props.data[rowId];
editValue = hasColumns ? item[retrieveFieldName(columns[colId])] : item[retrieveFieldName(instance._columns[colId])];
newState = {
editableRow: rowId,
editableCol: colId,
editValue
};
if (props.editable && props.multiEdit) {
if (shiftClick) {
selectedRangeStart = state.selectedRangeStart;
if (selectedRangeStart) {
prevRowId = selectedRangeStart.y;
prevColId = selectedRangeStart.x;
if ((typeof prevRowId==='number') && (typeof prevColId==='number') && ((prevColId!==colId) || (prevRowId!==rowId))) {
newState.selectedRange = {
x1: Math.min(prevColId, colId),
y1: Math.min(prevRowId, rowId),
x2: Math.max(prevColId, colId),
y2: Math.max(prevRowId, rowId)
};
}
}
else {
newState.selectedRange = null;
}
}
else {
newState.selectedRange = null;
newState.selectedRangeStart = {
x: colId,
y: rowId
};
}
}
instance.setState(newState);
instance._focusActiveCell();
}
onClick && onClick(rowId, colId);
}
addRow(index) {
let newData, len, newRecord;
const props = this.props,
onChange = props.onChange;
if (onChange) {
newData = cloneData(props.data);
len = newData.length;
if (len==0) {
newData = [{'__row0': null}];
}
else {
newRecord = newData[0].itsa_map(() => null);
if (typeof index==='number') {
newData.itsa_insertAt(newRecord, index);
}
else {
newData[props.extendableY==="begin" ? "unshift" : "push"](newRecord);
}
}
onChange(newData);
}
}
addCol() {
let newData, len, size;
const props = this.props,
onChange = props.onChange;
if (onChange) {
newData = cloneData(props.data);
len = newData.length;
if (len==0) {
size = 0;
}
else {
size = newData[0].itsa_size();
}
newData.forEach(record => {
record['__col'+size] = null;
});
onChange(newData);
}
}
deleteRow(index) {
let newData;
const props = this.props,
onChange = props.onChange;
if (onChange) {
newData = cloneData(props.data);
newData.splice(index, 1);
onChange(newData);
}
}
/**
* React render-method --> renderes the Component.
*
* @method render
* @return ReactComponent
* @since 2.0.0
*/
render() {
let classname = MAIN_CLASS,
handleClick, refocus, addRowBtn, addColBtn, tableClassName, addRowClass;
const instance = this,
props = instance.props,
editable = props.editable,
disabled = props.disabled,
propsClassName = props.className;
tableClassName = props.tableClass;
propsClassName && (classname+=' '+propsClassName);
if (props.extendableY) {
addRowClass = 'add-row controll-btn';
if ((props.removeableY) && (props.extendableY==='full')) {
addRowClass += ' add-row-indent';
}
addRowBtn = (<Button buttonText="+" className={addRowClass} disabled={disabled} onClick={instance.addRow} />);
}
if (props.extendableX && !props.columns) {
addColBtn = (<Button buttonText="+" className="controll-btn" disabled={disabled} onClick={instance.addCol} />);
}
if ((editable===true) || (editable==='full')) {
handleClick = instance.handleClick.bind(instance, true);
refocus = instance.refocus;
}
else {
handleClick = instance.handleClick.bind(instance, false);
}
if (props.fixedHeaders) {
tableClassName += ' fixed-headers';
classname += ' scrollable-y';
// fixedHeadertable = (
// <table className={props.tableClass+' fixed-headers'}>
// {instance.generateHead()}
// <tbody>
// {instance.generateRows()}
// </tbody>
// </table>
// );
}
// classname+='flex-container-vertical';
return (
<div
className={classname}
onScroll={props.onScroll}
ref={node => instance._componentNode = node}>
<table
className={tableClassName}
ref={node => instance._tableNode=node}>
{instance.generateHead()}
<tbody
onClick={handleClick}
onKeyDown={refocus}>
{instance.generateRows()}
</tbody>
</table>
{addColBtn}
{addRowBtn}
</div>
);
}
/**
* Callback for a click on the document. Is needed to close the Component when clicked outside.
*
* @method _handleDocumentClick
* @private
* @param Object e
* @since 0.0.1
*/
_handleDocumentClick(e) {
let rowIndex, field, colIndex, columns, hasColumns;
const instance = this,
targetNode = e.target;
if ((instance.props.editable===true) && (!instance._componentNode.contains(targetNode) || ((targetNode.tagName!=='TEXTAREA') && !instance._headNode.contains(targetNode)))) {
rowIndex = instance.state.editableRow;
colIndex = instance.state.editableCol;
columns = instance.props.columns;
hasColumns = (columns && (columns.length>0));
field = hasColumns ? columns[colIndex] : instance._columns[colIndex];
instance.setState({
editableRow: null,
editableCol: null,
editValue: ''
}, () => {
(typeof rowIndex==='number') && instance.implementCellChanges(rowIndex, field);
});
}
}
}
Table.propTypes = {
autoFocus: PropTypes.bool,
columns: PropTypes.array,
/**
* The Component its children
*
* @property children
* @type Object
* @since 2.0.0
*/
cursorNav: PropTypes.bool,
data: PropTypes.array,
disabled: PropTypes.bool,
editable: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
editableCols: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
editDirectionDown: PropTypes.bool,
extendableX: PropTypes.bool,
extendableY: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), // t