@reactual/handsontable
Version:
Spreadsheet-like data grid editor
343 lines (280 loc) • 9.54 kB
JavaScript
import {empty, addClass, hasClass} from './../helpers/dom/element';
import {equalsIgnoreCase} from './../helpers/string';
import EventManager from './../eventManager';
import {isKey} from './../helpers/unicode';
import {partial} from './../helpers/function';
import {stopImmediatePropagation, isImmediatePropagationStopped} from './../helpers/dom/event';
import {getRenderer} from './index';
const isListeningKeyDownEvent = new WeakMap();
const isCheckboxListenerAdded = new WeakMap();
const BAD_VALUE_CLASS = 'htBadValue';
/**
* Checkbox renderer
*
* @private
* @param {Object} instance Handsontable instance
* @param {Element} TD Table cell where to render
* @param {Number} row
* @param {Number} col
* @param {String|Number} prop Row object property name
* @param value Value to render (remember to escape unsafe HTML before inserting to DOM!)
* @param {Object} cellProperties Cell properties (shared by cell renderer and editor)
*/
function checkboxRenderer(instance, TD, row, col, prop, value, cellProperties) {
getRenderer('base').apply(this, arguments);
const eventManager = registerEvents(instance);
let input = createInput();
const labelOptions = cellProperties.label;
let badValue = false;
if (typeof cellProperties.checkedTemplate === 'undefined') {
cellProperties.checkedTemplate = true;
}
if (typeof cellProperties.uncheckedTemplate === 'undefined') {
cellProperties.uncheckedTemplate = false;
}
empty(TD); // TODO identify under what circumstances this line can be removed
if (value === cellProperties.checkedTemplate || equalsIgnoreCase(value, cellProperties.checkedTemplate)) {
input.checked = true;
} else if (value === cellProperties.uncheckedTemplate || equalsIgnoreCase(value, cellProperties.uncheckedTemplate)) {
input.checked = false;
} else if (value === null) { // default value
addClass(input, 'noValue');
} else {
input.style.display = 'none';
addClass(input, BAD_VALUE_CLASS);
badValue = true;
}
input.setAttribute('data-row', row);
input.setAttribute('data-col', col);
if (!badValue && labelOptions) {
let labelText = '';
if (labelOptions.value) {
labelText = typeof labelOptions.value === 'function' ? labelOptions.value.call(this, row, col, prop, value) : labelOptions.value;
} else if (labelOptions.property) {
labelText = instance.getDataAtRowProp(row, labelOptions.property);
}
const label = createLabel(labelText);
if (labelOptions.position === 'before') {
label.appendChild(input);
} else {
label.insertBefore(input, label.firstChild);
}
input = label;
}
TD.appendChild(input);
if (badValue) {
TD.appendChild(document.createTextNode('#bad-value#'));
}
if (!isListeningKeyDownEvent.has(instance)) {
isListeningKeyDownEvent.set(instance, true);
instance.addHook('beforeKeyDown', onBeforeKeyDown);
}
/**
* On before key down DOM listener.
*
* @private
* @param {Event} event
*/
function onBeforeKeyDown(event) {
const toggleKeys = 'SPACE|ENTER';
const switchOffKeys = 'DELETE|BACKSPACE';
const isKeyCode = partial(isKey, event.keyCode);
if (isKeyCode(`${toggleKeys}|${switchOffKeys}`) && !isImmediatePropagationStopped(event)) {
eachSelectedCheckboxCell(() => {
stopImmediatePropagation(event);
event.preventDefault();
});
}
if (isKeyCode(toggleKeys)) {
changeSelectedCheckboxesState();
}
if (isKeyCode(switchOffKeys)) {
changeSelectedCheckboxesState(true);
}
}
/**
* Change checkbox checked property
*
* @private
* @param {Boolean} [uncheckCheckbox=false]
*/
function changeSelectedCheckboxesState(uncheckCheckbox = false) {
const selRange = instance.getSelectedRange();
if (!selRange) {
return;
}
const topLeft = selRange.getTopLeftCorner();
const bottomRight = selRange.getBottomRightCorner();
const changes = [];
for (let row = topLeft.row; row <= bottomRight.row; row += 1) {
for (let col = topLeft.col; col <= bottomRight.col; col += 1) {
const cellProperties = instance.getCellMeta(row, col);
if (cellProperties.type !== 'checkbox') {
return;
}
/* eslint-disable no-continue */
if (cellProperties.readOnly === true) {
continue;
}
if (typeof cellProperties.checkedTemplate === 'undefined') {
cellProperties.checkedTemplate = true;
}
if (typeof cellProperties.uncheckedTemplate === 'undefined') {
cellProperties.uncheckedTemplate = false;
}
const dataAtCell = instance.getDataAtCell(row, col);
if (uncheckCheckbox === false) {
if (dataAtCell === cellProperties.checkedTemplate) {
changes.push([row, col, cellProperties.uncheckedTemplate]);
} else if ([cellProperties.uncheckedTemplate, null, void 0].indexOf(dataAtCell) !== -1) {
changes.push([row, col, cellProperties.checkedTemplate]);
}
} else {
changes.push([row, col, cellProperties.uncheckedTemplate]);
}
}
}
if (changes.length > 0) {
instance.setDataAtCell(changes);
}
}
/**
* Call callback for each found selected cell with checkbox type.
*
* @private
* @param {Function} callback
*/
function eachSelectedCheckboxCell(callback) {
const selRange = instance.getSelectedRange();
if (!selRange) {
return;
}
const topLeft = selRange.getTopLeftCorner();
const bottomRight = selRange.getBottomRightCorner();
for (let row = topLeft.row; row <= bottomRight.row; row++) {
for (let col = topLeft.col; col <= bottomRight.col; col++) {
let cellProperties = instance.getCellMeta(row, col);
if (cellProperties.type !== 'checkbox') {
return;
}
let cell = instance.getCell(row, col);
if (cell == null) {
callback(row, col, cellProperties);
} else {
let checkboxes = cell.querySelectorAll('input[type=checkbox]');
if (checkboxes.length > 0 && !cellProperties.readOnly) {
callback(checkboxes);
}
}
}
}
}
}
/**
* Register checkbox listeners.
*
* @param {Handsontable} instance Handsontable instance.
* @returns {EventManager}
*/
function registerEvents(instance) {
let eventManager = isCheckboxListenerAdded.get(instance);
if (!eventManager) {
eventManager = new EventManager(instance);
eventManager.addEventListener(instance.rootElement, 'click', (event) => onClick(event, instance));
eventManager.addEventListener(instance.rootElement, 'mouseup', (event) => onMouseUp(event, instance));
eventManager.addEventListener(instance.rootElement, 'change', (event) => onChange(event, instance));
isCheckboxListenerAdded.set(instance, eventManager);
}
return eventManager;
}
/**
* Create input element.
*
* @returns {Node}
*/
function createInput() {
let input = document.createElement('input');
input.className = 'htCheckboxRendererInput';
input.type = 'checkbox';
input.setAttribute('autocomplete', 'off');
input.setAttribute('tabindex', '-1');
return input.cloneNode(false);
}
/**
* Create label element.
*
* @returns {Node}
*/
function createLabel(text) {
let label = document.createElement('label');
label.className = 'htCheckboxRendererLabel';
label.appendChild(document.createTextNode(text));
return label.cloneNode(true);
}
/**
* `mouseup` callback.
*
* @private
* @param {Event} event `mouseup` event.
* @param {Object} instance Handsontable instance.
*/
function onMouseUp(event, instance) {
if (!isCheckboxInput(event.target)) {
return;
}
setTimeout(instance.listen, 10);
}
/**
* `click` callback.
*
* @private
* @param {Event} event `click` event.
* @param {Object} instance Handsontable instance.
*/
function onClick(event, instance) {
if (!isCheckboxInput(event.target)) {
return false;
}
const row = parseInt(event.target.getAttribute('data-row'), 10);
const col = parseInt(event.target.getAttribute('data-col'), 10);
const cellProperties = instance.getCellMeta(row, col);
if (cellProperties.readOnly) {
event.preventDefault();
}
}
/**
* `change` callback.
*
* @param {Event} event `change` event.
* @param {Object} instance Handsontable instance.
* @param {Object} cellProperties Reference to cell properties.
* @returns {Boolean}
*/
function onChange(event, instance) {
if (!isCheckboxInput(event.target)) {
return false;
}
const row = parseInt(event.target.getAttribute('data-row'), 10);
const col = parseInt(event.target.getAttribute('data-col'), 10);
const cellProperties = instance.getCellMeta(row, col);
if (!cellProperties.readOnly) {
let newCheckboxValue = null;
if (event.target.checked) {
newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? true : cellProperties.checkedTemplate;
} else {
newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? false : cellProperties.uncheckedTemplate;
}
instance.setDataAtCell(row, col, newCheckboxValue);
}
}
/**
* Check if the provided element is the checkbox input.
*
* @private
* @param {HTMLElement} element The element in question.
* @returns {Boolean}
*/
function isCheckboxInput(element) {
return element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}
export default checkboxRenderer;