7834628326e67ccada5324537879f21c3880c852a5cd39c8b47d5426aa6a8b2b1bf2451e2a2437a5b8fc537dac37c78e9aa85f278d1ea127a3410268bb87c7 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import {empty, addClass, hasClass} 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) {
  24. getRenderer('base').apply(this, arguments);
  25. const eventManager = 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 (isKeyCode(`${toggleKeys}|${switchOffKeys}`) && !isImmediatePropagationStopped(event)) {
  83. eachSelectedCheckboxCell(() => {
  84. stopImmediatePropagation(event);
  85. event.preventDefault();
  86. });
  87. }
  88. if (isKeyCode(toggleKeys)) {
  89. changeSelectedCheckboxesState();
  90. }
  91. if (isKeyCode(switchOffKeys)) {
  92. changeSelectedCheckboxesState(true);
  93. }
  94. }
  95. /**
  96. * Change checkbox checked property
  97. *
  98. * @private
  99. * @param {Boolean} [uncheckCheckbox=false]
  100. */
  101. function changeSelectedCheckboxesState(uncheckCheckbox = false) {
  102. const selRange = instance.getSelectedRange();
  103. if (!selRange) {
  104. return;
  105. }
  106. const topLeft = selRange.getTopLeftCorner();
  107. const bottomRight = selRange.getBottomRightCorner();
  108. const changes = [];
  109. for (let row = topLeft.row; row <= bottomRight.row; row += 1) {
  110. for (let col = topLeft.col; col <= bottomRight.col; col += 1) {
  111. const cellProperties = instance.getCellMeta(row, col);
  112. if (cellProperties.type !== 'checkbox') {
  113. return;
  114. }
  115. /* eslint-disable no-continue */
  116. if (cellProperties.readOnly === true) {
  117. continue;
  118. }
  119. if (typeof cellProperties.checkedTemplate === 'undefined') {
  120. cellProperties.checkedTemplate = true;
  121. }
  122. if (typeof cellProperties.uncheckedTemplate === 'undefined') {
  123. cellProperties.uncheckedTemplate = false;
  124. }
  125. const dataAtCell = instance.getDataAtCell(row, col);
  126. if (uncheckCheckbox === false) {
  127. if (dataAtCell === cellProperties.checkedTemplate) {
  128. changes.push([row, col, cellProperties.uncheckedTemplate]);
  129. } else if ([cellProperties.uncheckedTemplate, null, void 0].indexOf(dataAtCell) !== -1) {
  130. changes.push([row, col, cellProperties.checkedTemplate]);
  131. }
  132. } else {
  133. changes.push([row, col, cellProperties.uncheckedTemplate]);
  134. }
  135. }
  136. }
  137. if (changes.length > 0) {
  138. instance.setDataAtCell(changes);
  139. }
  140. }
  141. /**
  142. * Call callback for each found selected cell with checkbox type.
  143. *
  144. * @private
  145. * @param {Function} callback
  146. */
  147. function eachSelectedCheckboxCell(callback) {
  148. const selRange = instance.getSelectedRange();
  149. if (!selRange) {
  150. return;
  151. }
  152. const topLeft = selRange.getTopLeftCorner();
  153. const bottomRight = selRange.getBottomRightCorner();
  154. for (let row = topLeft.row; row <= bottomRight.row; row++) {
  155. for (let col = topLeft.col; col <= bottomRight.col; col++) {
  156. let cellProperties = instance.getCellMeta(row, col);
  157. if (cellProperties.type !== 'checkbox') {
  158. return;
  159. }
  160. let cell = instance.getCell(row, col);
  161. if (cell == null) {
  162. callback(row, col, cellProperties);
  163. } else {
  164. let checkboxes = cell.querySelectorAll('input[type=checkbox]');
  165. if (checkboxes.length > 0 && !cellProperties.readOnly) {
  166. callback(checkboxes);
  167. }
  168. }
  169. }
  170. }
  171. }
  172. }
  173. /**
  174. * Register checkbox listeners.
  175. *
  176. * @param {Handsontable} instance Handsontable instance.
  177. * @returns {EventManager}
  178. */
  179. function registerEvents(instance) {
  180. let eventManager = isCheckboxListenerAdded.get(instance);
  181. if (!eventManager) {
  182. eventManager = new EventManager(instance);
  183. eventManager.addEventListener(instance.rootElement, 'click', (event) => onClick(event, instance));
  184. eventManager.addEventListener(instance.rootElement, 'mouseup', (event) => onMouseUp(event, instance));
  185. eventManager.addEventListener(instance.rootElement, 'change', (event) => onChange(event, instance));
  186. isCheckboxListenerAdded.set(instance, eventManager);
  187. }
  188. return eventManager;
  189. }
  190. /**
  191. * Create input element.
  192. *
  193. * @returns {Node}
  194. */
  195. function createInput() {
  196. let input = document.createElement('input');
  197. input.className = 'htCheckboxRendererInput';
  198. input.type = 'checkbox';
  199. input.setAttribute('autocomplete', 'off');
  200. input.setAttribute('tabindex', '-1');
  201. return input.cloneNode(false);
  202. }
  203. /**
  204. * Create label element.
  205. *
  206. * @returns {Node}
  207. */
  208. function createLabel(text) {
  209. let label = document.createElement('label');
  210. label.className = 'htCheckboxRendererLabel';
  211. label.appendChild(document.createTextNode(text));
  212. return label.cloneNode(true);
  213. }
  214. /**
  215. * `mouseup` callback.
  216. *
  217. * @private
  218. * @param {Event} event `mouseup` event.
  219. * @param {Object} instance Handsontable instance.
  220. */
  221. function onMouseUp(event, instance) {
  222. if (!isCheckboxInput(event.target)) {
  223. return;
  224. }
  225. setTimeout(instance.listen, 10);
  226. }
  227. /**
  228. * `click` callback.
  229. *
  230. * @private
  231. * @param {Event} event `click` event.
  232. * @param {Object} instance Handsontable instance.
  233. */
  234. function onClick(event, instance) {
  235. if (!isCheckboxInput(event.target)) {
  236. return false;
  237. }
  238. const row = parseInt(event.target.getAttribute('data-row'), 10);
  239. const col = parseInt(event.target.getAttribute('data-col'), 10);
  240. const cellProperties = instance.getCellMeta(row, col);
  241. if (cellProperties.readOnly) {
  242. event.preventDefault();
  243. }
  244. }
  245. /**
  246. * `change` callback.
  247. *
  248. * @param {Event} event `change` event.
  249. * @param {Object} instance Handsontable instance.
  250. * @param {Object} cellProperties Reference to cell properties.
  251. * @returns {Boolean}
  252. */
  253. function onChange(event, instance) {
  254. if (!isCheckboxInput(event.target)) {
  255. return false;
  256. }
  257. const row = parseInt(event.target.getAttribute('data-row'), 10);
  258. const col = parseInt(event.target.getAttribute('data-col'), 10);
  259. const cellProperties = instance.getCellMeta(row, col);
  260. if (!cellProperties.readOnly) {
  261. let newCheckboxValue = null;
  262. if (event.target.checked) {
  263. newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? true : cellProperties.checkedTemplate;
  264. } else {
  265. newCheckboxValue = cellProperties.uncheckedTemplate === void 0 ? false : cellProperties.uncheckedTemplate;
  266. }
  267. instance.setDataAtCell(row, col, newCheckboxValue);
  268. }
  269. }
  270. /**
  271. * Check if the provided element is the checkbox input.
  272. *
  273. * @private
  274. * @param {HTMLElement} element The element in question.
  275. * @returns {Boolean}
  276. */
  277. function isCheckboxInput(element) {
  278. return element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
  279. }
  280. export default checkboxRenderer;