checkboxRenderer.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import { empty, addClass } from './../helpers/dom/element';
  2. import { equalsIgnoreCase } from './../helpers/string';
  3. import EventManager from './../eventManager';
  4. import { isKey } from './../helpers/unicode';
  5. import { partial } from './../helpers/function';
  6. import { stopImmediatePropagation, isImmediatePropagationStopped } from './../helpers/dom/event';
  7. import { getRenderer } from './index';
  8. const isListeningKeyDownEvent = new WeakMap();
  9. const isCheckboxListenerAdded = new WeakMap();
  10. const BAD_VALUE_CLASS = 'htBadValue';
  11. /**
  12. * Checkbox renderer
  13. *
  14. * @private
  15. * @param {Object} instance Handsontable instance
  16. * @param {Element} TD Table cell where to render
  17. * @param {Number} row
  18. * @param {Number} col
  19. * @param {String|Number} prop Row object property name
  20. * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!)
  21. * @param {Object} cellProperties Cell properties (shared by cell renderer and editor)
  22. */
  23. function checkboxRenderer(instance, TD, row, col, prop, value, cellProperties, ...args) {
  24. getRenderer('base').apply(this, [instance, TD, row, col, prop, value, cellProperties, ...args]);
  25. registerEvents(instance);
  26. let input = createInput();
  27. const labelOptions = cellProperties.label;
  28. let badValue = false;
  29. if (typeof cellProperties.checkedTemplate === 'undefined') {
  30. cellProperties.checkedTemplate = true;
  31. }
  32. if (typeof cellProperties.uncheckedTemplate === 'undefined') {
  33. cellProperties.uncheckedTemplate = false;
  34. }
  35. empty(TD); // TODO identify under what circumstances this line can be removed
  36. if (value === cellProperties.checkedTemplate || equalsIgnoreCase(value, cellProperties.checkedTemplate)) {
  37. input.checked = true;
  38. } else if (value === cellProperties.uncheckedTemplate || equalsIgnoreCase(value, cellProperties.uncheckedTemplate)) {
  39. input.checked = false;
  40. } else if (value === null) { // default value
  41. addClass(input, 'noValue');
  42. } else {
  43. input.style.display = 'none';
  44. addClass(input, BAD_VALUE_CLASS);
  45. badValue = true;
  46. }
  47. input.setAttribute('data-row', row);
  48. input.setAttribute('data-col', col);
  49. if (!badValue && labelOptions) {
  50. let labelText = '';
  51. if (labelOptions.value) {
  52. labelText = typeof labelOptions.value === 'function' ? labelOptions.value.call(this, row, col, prop, value) : labelOptions.value;
  53. } else if (labelOptions.property) {
  54. labelText = instance.getDataAtRowProp(row, labelOptions.property);
  55. }
  56. const label = createLabel(labelText);
  57. if (labelOptions.position === 'before') {
  58. label.appendChild(input);
  59. } else {
  60. label.insertBefore(input, label.firstChild);
  61. }
  62. input = label;
  63. }
  64. TD.appendChild(input);
  65. if (badValue) {
  66. TD.appendChild(document.createTextNode('#bad-value#'));
  67. }
  68. if (!isListeningKeyDownEvent.has(instance)) {
  69. isListeningKeyDownEvent.set(instance, true);
  70. instance.addHook('beforeKeyDown', onBeforeKeyDown);
  71. }
  72. /**
  73. * On before key down DOM listener.
  74. *
  75. * @private
  76. * @param {Event} event
  77. */
  78. function onBeforeKeyDown(event) {
  79. const toggleKeys = 'SPACE|ENTER';
  80. const switchOffKeys = 'DELETE|BACKSPACE';
  81. const isKeyCode = partial(isKey, event.keyCode);
  82. if (!instance.getSettings().enterBeginsEditing && isKeyCode('ENTER')) {
  83. return;
  84. }
  85. if (isKeyCode(`${toggleKeys}|${switchOffKeys}`) && !isImmediatePropagationStopped(event)) {
  86. eachSelectedCheckboxCell(() => {
  87. stopImmediatePropagation(event);
  88. event.preventDefault();
  89. });
  90. }
  91. if (isKeyCode(toggleKeys)) {
  92. changeSelectedCheckboxesState();
  93. }
  94. if (isKeyCode(switchOffKeys)) {
  95. changeSelectedCheckboxesState(true);
  96. }
  97. }
  98. /**
  99. * Change checkbox checked property
  100. *
  101. * @private
  102. * @param {Boolean} [uncheckCheckbox=false]
  103. */
  104. function changeSelectedCheckboxesState(uncheckCheckbox = false) {
  105. const selRange = instance.getSelectedRangeLast();
  106. if (!selRange) {
  107. return;
  108. }
  109. const { row: startRow, col: startColumn } = selRange.getTopLeftCorner();
  110. const { row: endRow, col: endColumn } = selRange.getBottomRightCorner();
  111. const changes = [];
  112. for (let visualRow = startRow; visualRow <= endRow; visualRow += 1) {
  113. for (let visualColumn = startColumn; visualColumn <= endColumn; visualColumn += 1) {
  114. const cachedCellProperties = instance.getCellMeta(visualRow, visualColumn);
  115. if (cachedCellProperties.type !== 'checkbox') {
  116. return;
  117. }
  118. /* eslint-disable no-continue */
  119. if (cachedCellProperties.readOnly === true) {
  120. continue;
  121. }
  122. if (typeof cachedCellProperties.checkedTemplate === 'undefined') {
  123. cachedCellProperties.checkedTemplate = true;
  124. }
  125. if (typeof cachedCellProperties.uncheckedTemplate === 'undefined') {
  126. cachedCellProperties.uncheckedTemplate = false;
  127. }
  128. const dataAtCell = instance.getDataAtCell(visualRow, visualColumn);
  129. if (uncheckCheckbox === false) {
  130. if ([cachedCellProperties.checkedTemplate, cachedCellProperties.checkedTemplate.toString()].includes(dataAtCell)) {
  131. changes.push([visualRow, visualColumn, cachedCellProperties.uncheckedTemplate]);
  132. } else if ([cachedCellProperties.uncheckedTemplate, cachedCellProperties.uncheckedTemplate.toString(), null, void 0].includes(dataAtCell)) {
  133. changes.push([visualRow, visualColumn, cachedCellProperties.checkedTemplate]);
  134. }
  135. } else {
  136. changes.push([visualRow, visualColumn, cachedCellProperties.uncheckedTemplate]);
  137. }
  138. }
  139. }
  140. if (changes.length > 0) {
  141. instance.setDataAtCell(changes);
  142. }
  143. }
  144. /**
  145. * Call callback for each found selected cell with checkbox type.
  146. *
  147. * @private
  148. * @param {Function} callback
  149. */
  150. function eachSelectedCheckboxCell(callback) {
  151. const selRange = instance.getSelectedRangeLast();
  152. if (!selRange) {
  153. return;
  154. }
  155. const topLeft = selRange.getTopLeftCorner();
  156. const bottomRight = selRange.getBottomRightCorner();
  157. for (let visualRow = topLeft.row; visualRow <= bottomRight.row; visualRow++) {
  158. for (let visualColumn = topLeft.col; visualColumn <= bottomRight.col; visualColumn++) {
  159. const cachedCellProperties = instance.getCellMeta(visualRow, visualColumn);
  160. if (cachedCellProperties.type !== 'checkbox') {
  161. return;
  162. }
  163. const cell = instance.getCell(visualRow, visualColumn);
  164. if (cell === null || cell === void 0) {
  165. callback(visualRow, visualColumn, cachedCellProperties);
  166. } else {
  167. const checkboxes = cell.querySelectorAll('input[type=checkbox]');
  168. if (checkboxes.length > 0 && !cachedCellProperties.readOnly) {
  169. callback(checkboxes);
  170. }
  171. }
  172. }
  173. }
  174. }
  175. }
  176. /**
  177. * Register checkbox listeners.
  178. *
  179. * @param {Handsontable} instance Handsontable instance.
  180. * @returns {EventManager}
  181. */
  182. function registerEvents(instance) {
  183. let eventManager = isCheckboxListenerAdded.get(instance);
  184. if (!eventManager) {
  185. eventManager = new EventManager(instance);
  186. eventManager.addEventListener(instance.rootElement, 'click', event => onClick(event, instance));
  187. eventManager.addEventListener(instance.rootElement, 'mouseup', event => onMouseUp(event, instance));
  188. eventManager.addEventListener(instance.rootElement, 'change', event => onChange(event, instance));
  189. isCheckboxListenerAdded.set(instance, eventManager);
  190. }
  191. return eventManager;
  192. }
  193. /**
  194. * Create input element.
  195. *
  196. * @returns {Node}
  197. */
  198. function createInput() {
  199. const input = document.createElement('input');
  200. input.className = 'htCheckboxRendererInput';
  201. input.type = 'checkbox';
  202. input.setAttribute('autocomplete', 'off');
  203. input.setAttribute('tabindex', '-1');
  204. return input.cloneNode(false);
  205. }
  206. /**
  207. * Create label element.
  208. *
  209. * @returns {Node}
  210. */
  211. function createLabel(text) {
  212. const label = document.createElement('label');
  213. label.className = 'htCheckboxRendererLabel';
  214. label.appendChild(document.createTextNode(text));
  215. return label.cloneNode(true);
  216. }
  217. /**
  218. * `mouseup` callback.
  219. *
  220. * @private
  221. * @param {Event} event `mouseup` event.
  222. * @param {Object} instance Handsontable instance.
  223. */
  224. function onMouseUp(event, instance) {
  225. if (!isCheckboxInput(event.target)) {
  226. return;
  227. }
  228. setTimeout(instance.listen, 10);
  229. }
  230. /**
  231. * `click` callback.
  232. *
  233. * @private
  234. * @param {Event} event `click` event.
  235. * @param {Object} instance Handsontable instance.
  236. */
  237. function onClick(event, instance) {
  238. if (!isCheckboxInput(event.target)) {
  239. return false;
  240. }
  241. const row = parseInt(event.target.getAttribute('data-row'), 10);
  242. const col = parseInt(event.target.getAttribute('data-col'), 10);
  243. const cellProperties = instance.getCellMeta(row, col);
  244. if (cellProperties.readOnly) {
  245. event.preventDefault();
  246. }
  247. }
  248. /**
  249. * `change` callback.
  250. *
  251. * @param {Event} event `change` event.
  252. * @param {Object} instance Handsontable instance.
  253. * @param {Object} cellProperties Reference to cell properties.
  254. * @returns {Boolean}
  255. */
  256. function onChange(event, instance) {
  257. if (!isCheckboxInput(event.target)) {
  258. return false;
  259. }
  260. const row = parseInt(event.target.getAttribute('data-row'), 10);
  261. const col = parseInt(event.target.getAttribute('data-col'), 10);
  262. const cellProperties = instance.getCellMeta(row, col);
  263. if (!cellProperties.readOnly) {
  264. let newCheckboxValue = null;
  265. if (event.target.checked) {
  266. newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? true : cellProperties.checkedTemplate;
  267. } else {
  268. newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? false : cellProperties.uncheckedTemplate;
  269. }
  270. instance.setDataAtCell(row, col, newCheckboxValue);
  271. }
  272. }
  273. /**
  274. * Check if the provided element is the checkbox input.
  275. *
  276. * @private
  277. * @param {HTMLElement} element The element in question.
  278. * @returns {Boolean}
  279. */
  280. function isCheckboxInput(element) {
  281. return element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
  282. }
  283. export default checkboxRenderer;