00eb81d900d24dd4527e30f014c8a76c8bdeff13391cca43183d6e1bb591dea4d532c3ee6b5ad53c3dc989b691590b949856bca44b8cb22645a6caa6f269e8 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import Hooks from './../../pluginHooks';
  2. import {registerPlugin} from './../../plugins';
  3. import {hasOwnProperty} from './../../helpers/object';
  4. import {CellRange, Selection} from './../../3rdparty/walkontable/src';
  5. function CustomBorders() {}
  6. /** *
  7. * Current instance (table where borders should be placed)
  8. */
  9. var instance;
  10. /**
  11. * This plugin enables an option to apply custom borders through the context menu (configurable with context menu key `borders`).
  12. *
  13. * To initialize Handsontable with predefined custom borders, provide cell coordinates and border styles in a form of an array.
  14. *
  15. * See [Custom Borders](http://docs.handsontable.com/demo-custom-borders.html) demo for more examples.
  16. *
  17. * @example
  18. * ```js
  19. * ...
  20. * customBorders: [
  21. * {range: {
  22. * from: {row: 1, col: 1},
  23. * to: {row: 3, col: 4}},
  24. * left: {},
  25. * right: {},
  26. * top: {},
  27. * bottom: {}
  28. * }
  29. * ],
  30. * ...
  31. *
  32. * // or
  33. * ...
  34. * customBorders: [
  35. * {row: 2, col: 2, left: {width: 2, color: 'red'},
  36. * right: {width: 1, color: 'green'}, top: '', bottom: ''}
  37. * ],
  38. * ...
  39. * ```
  40. * @private
  41. * @class CustomBorders
  42. * @plugin CustomBorders
  43. */
  44. /** *
  45. * Check if plugin should be enabled.
  46. */
  47. var checkEnable = function(customBorders) {
  48. if (typeof customBorders === 'boolean') {
  49. if (customBorders === true) {
  50. return true;
  51. }
  52. }
  53. if (typeof customBorders === 'object') {
  54. if (customBorders.length > 0) {
  55. return true;
  56. }
  57. }
  58. return false;
  59. };
  60. /** *
  61. * Initialize plugin.
  62. */
  63. var init = function() {
  64. if (checkEnable(this.getSettings().customBorders)) {
  65. if (!this.customBorders) {
  66. instance = this;
  67. this.customBorders = new CustomBorders();
  68. }
  69. }
  70. };
  71. /** *
  72. * Get index of border from the settings.
  73. *
  74. * @param {String} className
  75. * @returns {Number}
  76. */
  77. var getSettingIndex = function(className) {
  78. for (var i = 0; i < instance.view.wt.selections.length; i++) {
  79. if (instance.view.wt.selections[i].settings.className == className) {
  80. return i;
  81. }
  82. }
  83. return -1;
  84. };
  85. /** *
  86. * Insert WalkontableSelection instance into Walkontable settings.
  87. *
  88. * @param border
  89. */
  90. var insertBorderIntoSettings = function(border) {
  91. var coordinates = {
  92. row: border.row,
  93. col: border.col
  94. };
  95. var selection = new Selection(border, new CellRange(coordinates, coordinates, coordinates));
  96. var index = getSettingIndex(border.className);
  97. if (index >= 0) {
  98. instance.view.wt.selections[index] = selection;
  99. } else {
  100. instance.view.wt.selections.push(selection);
  101. }
  102. };
  103. /** *
  104. * Prepare borders from setting (single cell).
  105. *
  106. * @param {Number} row Row index.
  107. * @param {Number} col Column index.
  108. * @param borderObj
  109. */
  110. var prepareBorderFromCustomAdded = function(row, col, borderObj) {
  111. var border = createEmptyBorders(row, col);
  112. border = extendDefaultBorder(border, borderObj);
  113. this.setCellMeta(row, col, 'borders', border);
  114. insertBorderIntoSettings(border);
  115. };
  116. /** *
  117. * Prepare borders from setting (object).
  118. *
  119. * @param {Object} rowObj
  120. */
  121. var prepareBorderFromCustomAddedRange = function(rowObj) {
  122. var range = rowObj.range;
  123. for (var row = range.from.row; row <= range.to.row; row++) {
  124. for (var col = range.from.col; col <= range.to.col; col++) {
  125. var border = createEmptyBorders(row, col);
  126. var add = 0;
  127. if (row == range.from.row) {
  128. add++;
  129. if (hasOwnProperty(rowObj, 'top')) {
  130. border.top = rowObj.top;
  131. }
  132. }
  133. if (row == range.to.row) {
  134. add++;
  135. if (hasOwnProperty(rowObj, 'bottom')) {
  136. border.bottom = rowObj.bottom;
  137. }
  138. }
  139. if (col == range.from.col) {
  140. add++;
  141. if (hasOwnProperty(rowObj, 'left')) {
  142. border.left = rowObj.left;
  143. }
  144. }
  145. if (col == range.to.col) {
  146. add++;
  147. if (hasOwnProperty(rowObj, 'right')) {
  148. border.right = rowObj.right;
  149. }
  150. }
  151. if (add > 0) {
  152. this.setCellMeta(row, col, 'borders', border);
  153. insertBorderIntoSettings(border);
  154. }
  155. }
  156. }
  157. };
  158. /** *
  159. * Create separated class name for borders for each cell.
  160. *
  161. * @param {Number} row Row index.
  162. * @param {Number} col Column index.
  163. * @returns {String}
  164. */
  165. var createClassName = function(row, col) {
  166. return `border_row${row}col${col}`;
  167. };
  168. /** *
  169. * Create default single border for each position (top/right/bottom/left).
  170. *
  171. * @returns {Object} `{{width: number, color: string}}`
  172. */
  173. var createDefaultCustomBorder = function() {
  174. return {
  175. width: 1,
  176. color: '#000'
  177. };
  178. };
  179. /** *
  180. * Create default object for empty border.
  181. *
  182. * @returns {Object} `{{hide: boolean}}`
  183. */
  184. var createSingleEmptyBorder = function() {
  185. return {
  186. hide: true
  187. };
  188. };
  189. /** *
  190. * Create default Handsontable border object.
  191. *
  192. * @returns {Object} `{{width: number, color: string, cornerVisible: boolean}}`
  193. */
  194. var createDefaultHtBorder = function() {
  195. return {
  196. width: 1,
  197. color: '#000',
  198. cornerVisible: false,
  199. };
  200. };
  201. /** *
  202. * Prepare empty border for each cell with all custom borders hidden.
  203. *
  204. * @param {Number} row Row index.
  205. * @param {Number} col Column index.
  206. * @returns {Object} `{{className: *, border: *, row: *, col: *, top: {hide: boolean}, right: {hide: boolean}, bottom: {hide: boolean}, left: {hide: boolean}}}`
  207. */
  208. var createEmptyBorders = function(row, col) {
  209. return {
  210. className: createClassName(row, col),
  211. border: createDefaultHtBorder(),
  212. row,
  213. col,
  214. top: createSingleEmptyBorder(),
  215. right: createSingleEmptyBorder(),
  216. bottom: createSingleEmptyBorder(),
  217. left: createSingleEmptyBorder(),
  218. };
  219. };
  220. var extendDefaultBorder = function(defaultBorder, customBorder) {
  221. if (hasOwnProperty(customBorder, 'border')) {
  222. defaultBorder.border = customBorder.border;
  223. }
  224. if (hasOwnProperty(customBorder, 'top')) {
  225. defaultBorder.top = customBorder.top;
  226. }
  227. if (hasOwnProperty(customBorder, 'right')) {
  228. defaultBorder.right = customBorder.right;
  229. }
  230. if (hasOwnProperty(customBorder, 'bottom')) {
  231. defaultBorder.bottom = customBorder.bottom;
  232. }
  233. if (hasOwnProperty(customBorder, 'left')) {
  234. defaultBorder.left = customBorder.left;
  235. }
  236. return defaultBorder;
  237. };
  238. /**
  239. * Remove borders divs from DOM.
  240. *
  241. * @param borderClassName
  242. */
  243. var removeBordersFromDom = function(borderClassName) {
  244. var borders = document.querySelectorAll(`.${borderClassName}`);
  245. for (var i = 0; i < borders.length; i++) {
  246. if (borders[i]) {
  247. if (borders[i].nodeName != 'TD') {
  248. var parent = borders[i].parentNode;
  249. if (parent.parentNode) {
  250. parent.parentNode.removeChild(parent);
  251. }
  252. }
  253. }
  254. }
  255. };
  256. /** *
  257. * Remove border (triggered from context menu).
  258. *
  259. * @param {Number} row Row index.
  260. * @param {Number} col Column index.
  261. */
  262. var removeAllBorders = function(row, col) {
  263. var borderClassName = createClassName(row, col);
  264. removeBordersFromDom(borderClassName);
  265. this.removeCellMeta(row, col, 'borders');
  266. };
  267. /** *
  268. * Set borders for each cell re. to border position
  269. *
  270. * @param row
  271. * @param col
  272. * @param place
  273. * @param remove
  274. */
  275. var setBorder = function(row, col, place, remove) {
  276. var bordersMeta = this.getCellMeta(row, col).borders;
  277. if (!bordersMeta || bordersMeta.border == undefined) {
  278. bordersMeta = createEmptyBorders(row, col);
  279. }
  280. if (remove) {
  281. bordersMeta[place] = createSingleEmptyBorder();
  282. } else {
  283. bordersMeta[place] = createDefaultCustomBorder();
  284. }
  285. this.setCellMeta(row, col, 'borders', bordersMeta);
  286. var borderClassName = createClassName(row, col);
  287. removeBordersFromDom(borderClassName);
  288. insertBorderIntoSettings(bordersMeta);
  289. this.render();
  290. };
  291. /** *
  292. * Prepare borders based on cell and border position
  293. *
  294. * @param range
  295. * @param place
  296. * @param remove
  297. */
  298. var prepareBorder = function(range, place, remove) {
  299. if (range.from.row == range.to.row && range.from.col == range.to.col) {
  300. if (place == 'noBorders') {
  301. removeAllBorders.call(this, range.from.row, range.from.col);
  302. } else {
  303. setBorder.call(this, range.from.row, range.from.col, place, remove);
  304. }
  305. } else {
  306. switch (place) {
  307. case 'noBorders':
  308. for (var column = range.from.col; column <= range.to.col; column++) {
  309. for (var row = range.from.row; row <= range.to.row; row++) {
  310. removeAllBorders.call(this, row, column);
  311. }
  312. }
  313. break;
  314. case 'top':
  315. for (var topCol = range.from.col; topCol <= range.to.col; topCol++) {
  316. setBorder.call(this, range.from.row, topCol, place, remove);
  317. }
  318. break;
  319. case 'right':
  320. for (var rowRight = range.from.row; rowRight <= range.to.row; rowRight++) {
  321. setBorder.call(this, rowRight, range.to.col, place);
  322. }
  323. break;
  324. case 'bottom':
  325. for (var bottomCol = range.from.col; bottomCol <= range.to.col; bottomCol++) {
  326. setBorder.call(this, range.to.row, bottomCol, place);
  327. }
  328. break;
  329. case 'left':
  330. for (var rowLeft = range.from.row; rowLeft <= range.to.row; rowLeft++) {
  331. setBorder.call(this, rowLeft, range.from.col, place);
  332. }
  333. break;
  334. default:
  335. break;
  336. }
  337. }
  338. };
  339. /** *
  340. * Check if selection has border by className
  341. *
  342. * @param hot
  343. * @param direction
  344. */
  345. var checkSelectionBorders = function(hot, direction) {
  346. var atLeastOneHasBorder = false;
  347. hot.getSelectedRange().forAll((r, c) => {
  348. var metaBorders = hot.getCellMeta(r, c).borders;
  349. if (metaBorders) {
  350. if (direction) {
  351. if (!hasOwnProperty(metaBorders[direction], 'hide')) {
  352. atLeastOneHasBorder = true;
  353. return false; // breaks forAll
  354. }
  355. } else {
  356. atLeastOneHasBorder = true;
  357. return false; // breaks forAll
  358. }
  359. }
  360. });
  361. return atLeastOneHasBorder;
  362. };
  363. /** *
  364. * Mark label in contextMenu as selected
  365. *
  366. * @param label
  367. * @returns {string}
  368. */
  369. var markSelected = function(label) {
  370. return `<span class="selected">${String.fromCharCode(10003)}</span>${label}`; // workaround for https://github.com/handsontable/handsontable/issues/1946
  371. };
  372. /** *
  373. * Add border options to context menu
  374. *
  375. * @param defaultOptions
  376. */
  377. var addBordersOptionsToContextMenu = function(defaultOptions) {
  378. if (!this.getSettings().customBorders) {
  379. return;
  380. }
  381. defaultOptions.items.push({
  382. name: '---------',
  383. });
  384. defaultOptions.items.push({
  385. key: 'borders',
  386. name: 'Borders',
  387. disabled() {
  388. return this.selection.selectedHeader.corner;
  389. },
  390. submenu: {
  391. items: [
  392. {
  393. key: 'borders:top',
  394. name() {
  395. var label = 'Top';
  396. var hasBorder = checkSelectionBorders(this, 'top');
  397. if (hasBorder) {
  398. label = markSelected(label);
  399. }
  400. return label;
  401. },
  402. callback() {
  403. var hasBorder = checkSelectionBorders(this, 'top');
  404. prepareBorder.call(this, this.getSelectedRange(), 'top', hasBorder);
  405. },
  406. },
  407. {
  408. key: 'borders:right',
  409. name() {
  410. var label = 'Right';
  411. var hasBorder = checkSelectionBorders(this, 'right');
  412. if (hasBorder) {
  413. label = markSelected(label);
  414. }
  415. return label;
  416. },
  417. callback() {
  418. var hasBorder = checkSelectionBorders(this, 'right');
  419. prepareBorder.call(this, this.getSelectedRange(), 'right', hasBorder);
  420. },
  421. },
  422. {
  423. key: 'borders:bottom',
  424. name() {
  425. var label = 'Bottom';
  426. var hasBorder = checkSelectionBorders(this, 'bottom');
  427. if (hasBorder) {
  428. label = markSelected(label);
  429. }
  430. return label;
  431. },
  432. callback() {
  433. var hasBorder = checkSelectionBorders(this, 'bottom');
  434. prepareBorder.call(this, this.getSelectedRange(), 'bottom', hasBorder);
  435. },
  436. },
  437. {
  438. key: 'borders:left',
  439. name() {
  440. var label = 'Left';
  441. var hasBorder = checkSelectionBorders(this, 'left');
  442. if (hasBorder) {
  443. label = markSelected(label);
  444. }
  445. return label;
  446. },
  447. callback() {
  448. var hasBorder = checkSelectionBorders(this, 'left');
  449. prepareBorder.call(this, this.getSelectedRange(), 'left', hasBorder);
  450. },
  451. },
  452. {
  453. key: 'borders:no_borders',
  454. name: 'Remove border(s)',
  455. callback() {
  456. prepareBorder.call(this, this.getSelectedRange(), 'noBorders');
  457. },
  458. disabled() {
  459. return !checkSelectionBorders(this);
  460. }
  461. }
  462. ]
  463. }
  464. });
  465. };
  466. Hooks.getSingleton().add('beforeInit', init);
  467. Hooks.getSingleton().add('afterContextMenuDefaultOptions', addBordersOptionsToContextMenu);
  468. Hooks.getSingleton().add('afterInit', function() {
  469. var customBorders = this.getSettings().customBorders;
  470. if (customBorders) {
  471. for (var i = 0; i < customBorders.length; i++) {
  472. if (customBorders[i].range) {
  473. prepareBorderFromCustomAddedRange.call(this, customBorders[i]);
  474. } else {
  475. prepareBorderFromCustomAdded.call(this, customBorders[i].row, customBorders[i].col, customBorders[i]);
  476. }
  477. }
  478. this.render();
  479. this.view.wt.draw(true);
  480. }
  481. });