a3c52dc60b8dc312101e7111d5db6c493ed859038f521f99d2a6776e1b81143bcf8d46b1693335a7625cefc8d357fc2eca49c5828e9a3b9c86de4287493e59 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import {
  2. getComputedStyle,
  3. getTrimmingContainer,
  4. innerWidth,
  5. innerHeight,
  6. offset,
  7. outerHeight,
  8. outerWidth,
  9. } from './../../../helpers/dom/element';
  10. import {stopImmediatePropagation} from './../../../helpers/dom/event';
  11. import {hasOwnProperty} from './../../../helpers/object';
  12. import {isMobileBrowser} from './../../../helpers/browser';
  13. import EventManager from './../../../eventManager';
  14. import CellCoords from './cell/coords';
  15. import Overlay from './overlay/_base.js';
  16. /**
  17. *
  18. */
  19. class Border {
  20. /**
  21. * @param {Walkontable} wotInstance
  22. * @param {Object} settings
  23. */
  24. constructor(wotInstance, settings) {
  25. if (!settings) {
  26. return;
  27. }
  28. this.eventManager = new EventManager(wotInstance);
  29. this.instance = wotInstance;
  30. this.wot = wotInstance;
  31. this.settings = settings;
  32. this.mouseDown = false;
  33. this.main = null;
  34. this.top = null;
  35. this.left = null;
  36. this.bottom = null;
  37. this.right = null;
  38. this.topStyle = null;
  39. this.leftStyle = null;
  40. this.bottomStyle = null;
  41. this.rightStyle = null;
  42. this.cornerDefaultStyle = {
  43. width: '5px',
  44. height: '5px',
  45. borderWidth: '2px',
  46. borderStyle: 'solid',
  47. borderColor: '#FFF'
  48. };
  49. this.corner = null;
  50. this.cornerStyle = null;
  51. this.createBorders(settings);
  52. this.registerListeners();
  53. }
  54. /**
  55. * Register all necessary events
  56. */
  57. registerListeners() {
  58. this.eventManager.addEventListener(document.body, 'mousedown', () => this.onMouseDown());
  59. this.eventManager.addEventListener(document.body, 'mouseup', () => this.onMouseUp());
  60. for (let c = 0, len = this.main.childNodes.length; c < len; c++) {
  61. this.eventManager.addEventListener(this.main.childNodes[c], 'mouseenter', (event) => this.onMouseEnter(event, this.main.childNodes[c]));
  62. }
  63. }
  64. /**
  65. * Mouse down listener
  66. *
  67. * @private
  68. */
  69. onMouseDown() {
  70. this.mouseDown = true;
  71. }
  72. /**
  73. * Mouse up listener
  74. *
  75. * @private
  76. */
  77. onMouseUp() {
  78. this.mouseDown = false;
  79. }
  80. /**
  81. * Mouse enter listener for fragment selection functionality.
  82. *
  83. * @private
  84. * @param {Event} event Dom event
  85. * @param {HTMLElement} parentElement Part of border element.
  86. */
  87. onMouseEnter(event, parentElement) {
  88. if (!this.mouseDown || !this.wot.getSetting('hideBorderOnMouseDownOver')) {
  89. return;
  90. }
  91. event.preventDefault();
  92. stopImmediatePropagation(event);
  93. let _this = this;
  94. let bounds = parentElement.getBoundingClientRect();
  95. // Hide border to prevents selection jumping when fragmentSelection is enabled.
  96. parentElement.style.display = 'none';
  97. function isOutside(event) {
  98. if (event.clientY < Math.floor(bounds.top)) {
  99. return true;
  100. }
  101. if (event.clientY > Math.ceil(bounds.top + bounds.height)) {
  102. return true;
  103. }
  104. if (event.clientX < Math.floor(bounds.left)) {
  105. return true;
  106. }
  107. if (event.clientX > Math.ceil(bounds.left + bounds.width)) {
  108. return true;
  109. }
  110. }
  111. function handler(event) {
  112. if (isOutside(event)) {
  113. _this.eventManager.removeEventListener(document.body, 'mousemove', handler);
  114. parentElement.style.display = 'block';
  115. }
  116. }
  117. this.eventManager.addEventListener(document.body, 'mousemove', handler);
  118. }
  119. /**
  120. * Create border elements
  121. *
  122. * @param {Object} settings
  123. */
  124. createBorders(settings) {
  125. this.main = document.createElement('div');
  126. let borderDivs = ['top', 'left', 'bottom', 'right', 'corner'];
  127. let style = this.main.style;
  128. style.position = 'absolute';
  129. style.top = 0;
  130. style.left = 0;
  131. for (let i = 0; i < 5; i++) {
  132. let position = borderDivs[i];
  133. let div = document.createElement('div');
  134. div.className = `wtBorder ${this.settings.className || ''}`; // + borderDivs[i];
  135. if (this.settings[position] && this.settings[position].hide) {
  136. div.className += ' hidden';
  137. }
  138. style = div.style;
  139. style.backgroundColor = (this.settings[position] && this.settings[position].color) ? this.settings[position].color : settings.border.color;
  140. style.height = (this.settings[position] && this.settings[position].width) ? `${this.settings[position].width}px` : `${settings.border.width}px`;
  141. style.width = (this.settings[position] && this.settings[position].width) ? `${this.settings[position].width}px` : `${settings.border.width}px`;
  142. this.main.appendChild(div);
  143. }
  144. this.top = this.main.childNodes[0];
  145. this.left = this.main.childNodes[1];
  146. this.bottom = this.main.childNodes[2];
  147. this.right = this.main.childNodes[3];
  148. this.topStyle = this.top.style;
  149. this.leftStyle = this.left.style;
  150. this.bottomStyle = this.bottom.style;
  151. this.rightStyle = this.right.style;
  152. this.corner = this.main.childNodes[4];
  153. this.corner.className += ' corner';
  154. this.cornerStyle = this.corner.style;
  155. this.cornerStyle.width = this.cornerDefaultStyle.width;
  156. this.cornerStyle.height = this.cornerDefaultStyle.height;
  157. this.cornerStyle.border = [
  158. this.cornerDefaultStyle.borderWidth,
  159. this.cornerDefaultStyle.borderStyle,
  160. this.cornerDefaultStyle.borderColor
  161. ].join(' ');
  162. if (isMobileBrowser()) {
  163. this.createMultipleSelectorHandles();
  164. }
  165. this.disappear();
  166. if (!this.wot.wtTable.bordersHolder) {
  167. this.wot.wtTable.bordersHolder = document.createElement('div');
  168. this.wot.wtTable.bordersHolder.className = 'htBorders';
  169. this.wot.wtTable.spreader.appendChild(this.wot.wtTable.bordersHolder);
  170. }
  171. this.wot.wtTable.bordersHolder.insertBefore(this.main, this.wot.wtTable.bordersHolder.firstChild);
  172. }
  173. /**
  174. * Create multiple selector handler for mobile devices
  175. */
  176. createMultipleSelectorHandles() {
  177. this.selectionHandles = {
  178. topLeft: document.createElement('DIV'),
  179. topLeftHitArea: document.createElement('DIV'),
  180. bottomRight: document.createElement('DIV'),
  181. bottomRightHitArea: document.createElement('DIV')
  182. };
  183. let width = 10;
  184. let hitAreaWidth = 40;
  185. this.selectionHandles.topLeft.className = 'topLeftSelectionHandle';
  186. this.selectionHandles.topLeftHitArea.className = 'topLeftSelectionHandle-HitArea';
  187. this.selectionHandles.bottomRight.className = 'bottomRightSelectionHandle';
  188. this.selectionHandles.bottomRightHitArea.className = 'bottomRightSelectionHandle-HitArea';
  189. this.selectionHandles.styles = {
  190. topLeft: this.selectionHandles.topLeft.style,
  191. topLeftHitArea: this.selectionHandles.topLeftHitArea.style,
  192. bottomRight: this.selectionHandles.bottomRight.style,
  193. bottomRightHitArea: this.selectionHandles.bottomRightHitArea.style
  194. };
  195. let hitAreaStyle = {
  196. position: 'absolute',
  197. height: `${hitAreaWidth}px`,
  198. width: `${hitAreaWidth}px`,
  199. 'border-radius': `${parseInt(hitAreaWidth / 1.5, 10)}px`,
  200. };
  201. for (let prop in hitAreaStyle) {
  202. if (hasOwnProperty(hitAreaStyle, prop)) {
  203. this.selectionHandles.styles.bottomRightHitArea[prop] = hitAreaStyle[prop];
  204. this.selectionHandles.styles.topLeftHitArea[prop] = hitAreaStyle[prop];
  205. }
  206. }
  207. let handleStyle = {
  208. position: 'absolute',
  209. height: `${width}px`,
  210. width: `${width}px`,
  211. 'border-radius': `${parseInt(width / 1.5, 10)}px`,
  212. background: '#F5F5FF',
  213. border: '1px solid #4285c8'
  214. };
  215. for (let prop in handleStyle) {
  216. if (hasOwnProperty(handleStyle, prop)) {
  217. this.selectionHandles.styles.bottomRight[prop] = handleStyle[prop];
  218. this.selectionHandles.styles.topLeft[prop] = handleStyle[prop];
  219. }
  220. }
  221. this.main.appendChild(this.selectionHandles.topLeft);
  222. this.main.appendChild(this.selectionHandles.bottomRight);
  223. this.main.appendChild(this.selectionHandles.topLeftHitArea);
  224. this.main.appendChild(this.selectionHandles.bottomRightHitArea);
  225. }
  226. isPartRange(row, col) {
  227. if (this.wot.selections.area.cellRange) {
  228. if (row != this.wot.selections.area.cellRange.to.row || col != this.wot.selections.area.cellRange.to.col) {
  229. return true;
  230. }
  231. }
  232. return false;
  233. }
  234. updateMultipleSelectionHandlesPosition(row, col, top, left, width, height) {
  235. let handleWidth = parseInt(this.selectionHandles.styles.topLeft.width, 10);
  236. let hitAreaWidth = parseInt(this.selectionHandles.styles.topLeftHitArea.width, 10);
  237. this.selectionHandles.styles.topLeft.top = `${parseInt(top - handleWidth, 10)}px`;
  238. this.selectionHandles.styles.topLeft.left = `${parseInt(left - handleWidth, 10)}px`;
  239. this.selectionHandles.styles.topLeftHitArea.top = `${parseInt(top - ((hitAreaWidth / 4) * 3), 10)}px`;
  240. this.selectionHandles.styles.topLeftHitArea.left = `${parseInt(left - ((hitAreaWidth / 4) * 3), 10)}px`;
  241. this.selectionHandles.styles.bottomRight.top = `${parseInt(top + height, 10)}px`;
  242. this.selectionHandles.styles.bottomRight.left = `${parseInt(left + width, 10)}px`;
  243. this.selectionHandles.styles.bottomRightHitArea.top = `${parseInt(top + height - (hitAreaWidth / 4), 10)}px`;
  244. this.selectionHandles.styles.bottomRightHitArea.left = `${parseInt(left + width - (hitAreaWidth / 4), 10)}px`;
  245. if (this.settings.border.multipleSelectionHandlesVisible && this.settings.border.multipleSelectionHandlesVisible()) {
  246. this.selectionHandles.styles.topLeft.display = 'block';
  247. this.selectionHandles.styles.topLeftHitArea.display = 'block';
  248. if (this.isPartRange(row, col)) {
  249. this.selectionHandles.styles.bottomRight.display = 'none';
  250. this.selectionHandles.styles.bottomRightHitArea.display = 'none';
  251. } else {
  252. this.selectionHandles.styles.bottomRight.display = 'block';
  253. this.selectionHandles.styles.bottomRightHitArea.display = 'block';
  254. }
  255. } else {
  256. this.selectionHandles.styles.topLeft.display = 'none';
  257. this.selectionHandles.styles.bottomRight.display = 'none';
  258. this.selectionHandles.styles.topLeftHitArea.display = 'none';
  259. this.selectionHandles.styles.bottomRightHitArea.display = 'none';
  260. }
  261. if (row == this.wot.wtSettings.getSetting('fixedRowsTop') || col == this.wot.wtSettings.getSetting('fixedColumnsLeft')) {
  262. this.selectionHandles.styles.topLeft.zIndex = '9999';
  263. this.selectionHandles.styles.topLeftHitArea.zIndex = '9999';
  264. } else {
  265. this.selectionHandles.styles.topLeft.zIndex = '';
  266. this.selectionHandles.styles.topLeftHitArea.zIndex = '';
  267. }
  268. }
  269. /**
  270. * Show border around one or many cells
  271. *
  272. * @param {Array} corners
  273. */
  274. appear(corners) {
  275. if (this.disabled) {
  276. return;
  277. }
  278. var isMultiple,
  279. fromTD,
  280. toTD,
  281. fromOffset,
  282. toOffset,
  283. containerOffset,
  284. top,
  285. minTop,
  286. left,
  287. minLeft,
  288. height,
  289. width,
  290. fromRow,
  291. fromColumn,
  292. toRow,
  293. toColumn,
  294. trimmingContainer,
  295. cornerOverlappingContainer,
  296. ilen;
  297. ilen = this.wot.wtTable.getRenderedRowsCount();
  298. for (let i = 0; i < ilen; i++) {
  299. let s = this.wot.wtTable.rowFilter.renderedToSource(i);
  300. if (s >= corners[0] && s <= corners[2]) {
  301. fromRow = s;
  302. break;
  303. }
  304. }
  305. for (let i = ilen - 1; i >= 0; i--) {
  306. let s = this.wot.wtTable.rowFilter.renderedToSource(i);
  307. if (s >= corners[0] && s <= corners[2]) {
  308. toRow = s;
  309. break;
  310. }
  311. }
  312. ilen = this.wot.wtTable.getRenderedColumnsCount();
  313. for (let i = 0; i < ilen; i++) {
  314. let s = this.wot.wtTable.columnFilter.renderedToSource(i);
  315. if (s >= corners[1] && s <= corners[3]) {
  316. fromColumn = s;
  317. break;
  318. }
  319. }
  320. for (let i = ilen - 1; i >= 0; i--) {
  321. let s = this.wot.wtTable.columnFilter.renderedToSource(i);
  322. if (s >= corners[1] && s <= corners[3]) {
  323. toColumn = s;
  324. break;
  325. }
  326. }
  327. if (fromRow === void 0 || fromColumn === void 0) {
  328. this.disappear();
  329. return;
  330. }
  331. isMultiple = (fromRow !== toRow || fromColumn !== toColumn);
  332. fromTD = this.wot.wtTable.getCell(new CellCoords(fromRow, fromColumn));
  333. toTD = isMultiple ? this.wot.wtTable.getCell(new CellCoords(toRow, toColumn)) : fromTD;
  334. fromOffset = offset(fromTD);
  335. toOffset = isMultiple ? offset(toTD) : fromOffset;
  336. containerOffset = offset(this.wot.wtTable.TABLE);
  337. minTop = fromOffset.top;
  338. height = toOffset.top + outerHeight(toTD) - minTop;
  339. minLeft = fromOffset.left;
  340. width = toOffset.left + outerWidth(toTD) - minLeft;
  341. top = minTop - containerOffset.top - 1;
  342. left = minLeft - containerOffset.left - 1;
  343. let style = getComputedStyle(fromTD);
  344. if (parseInt(style.borderTopWidth, 10) > 0) {
  345. top += 1;
  346. height = height > 0 ? height - 1 : 0;
  347. }
  348. if (parseInt(style.borderLeftWidth, 10) > 0) {
  349. left += 1;
  350. width = width > 0 ? width - 1 : 0;
  351. }
  352. this.topStyle.top = `${top}px`;
  353. this.topStyle.left = `${left}px`;
  354. this.topStyle.width = `${width}px`;
  355. this.topStyle.display = 'block';
  356. this.leftStyle.top = `${top}px`;
  357. this.leftStyle.left = `${left}px`;
  358. this.leftStyle.height = `${height}px`;
  359. this.leftStyle.display = 'block';
  360. let delta = Math.floor(this.settings.border.width / 2);
  361. this.bottomStyle.top = `${top + height - delta}px`;
  362. this.bottomStyle.left = `${left}px`;
  363. this.bottomStyle.width = `${width}px`;
  364. this.bottomStyle.display = 'block';
  365. this.rightStyle.top = `${top}px`;
  366. this.rightStyle.left = `${left + width - delta}px`;
  367. this.rightStyle.height = `${height + 1}px`;
  368. this.rightStyle.display = 'block';
  369. if (isMobileBrowser() || (!this.hasSetting(this.settings.border.cornerVisible) || this.isPartRange(toRow, toColumn))) {
  370. this.cornerStyle.display = 'none';
  371. } else {
  372. this.cornerStyle.top = `${top + height - 4}px`;
  373. this.cornerStyle.left = `${left + width - 4}px`;
  374. this.cornerStyle.borderRightWidth = this.cornerDefaultStyle.borderWidth;
  375. this.cornerStyle.width = this.cornerDefaultStyle.width;
  376. // Hide the fill handle, so the possible further adjustments won't force unneeded scrollbars.
  377. this.cornerStyle.display = 'none';
  378. trimmingContainer = getTrimmingContainer(this.wot.wtTable.TABLE);
  379. if (toColumn === this.wot.getSetting('totalColumns') - 1) {
  380. cornerOverlappingContainer = toTD.offsetLeft + outerWidth(toTD) + (parseInt(this.cornerDefaultStyle.width, 10) / 2) >= innerWidth(trimmingContainer);
  381. if (cornerOverlappingContainer) {
  382. this.cornerStyle.left = `${Math.floor(left + width - 3 - (parseInt(this.cornerDefaultStyle.width, 10) / 2))}px`;
  383. this.cornerStyle.borderRightWidth = 0;
  384. }
  385. }
  386. if (toRow === this.wot.getSetting('totalRows') - 1) {
  387. cornerOverlappingContainer = toTD.offsetTop + outerHeight(toTD) + (parseInt(this.cornerDefaultStyle.height, 10) / 2) >= innerHeight(trimmingContainer);
  388. if (cornerOverlappingContainer) {
  389. this.cornerStyle.top = `${Math.floor(top + height - 3 - (parseInt(this.cornerDefaultStyle.height, 10) / 2))}px`;
  390. this.cornerStyle.borderBottomWidth = 0;
  391. }
  392. }
  393. this.cornerStyle.display = 'block';
  394. }
  395. if (isMobileBrowser()) {
  396. this.updateMultipleSelectionHandlesPosition(fromRow, fromColumn, top, left, width, height);
  397. }
  398. }
  399. /**
  400. * Hide border
  401. */
  402. disappear() {
  403. this.topStyle.display = 'none';
  404. this.leftStyle.display = 'none';
  405. this.bottomStyle.display = 'none';
  406. this.rightStyle.display = 'none';
  407. this.cornerStyle.display = 'none';
  408. if (isMobileBrowser()) {
  409. this.selectionHandles.styles.topLeft.display = 'none';
  410. this.selectionHandles.styles.bottomRight.display = 'none';
  411. }
  412. }
  413. /**
  414. * @param {Function} setting
  415. * @returns {*}
  416. */
  417. hasSetting(setting) {
  418. if (typeof setting === 'function') {
  419. return setting();
  420. }
  421. return !!setting;
  422. }
  423. }
  424. export default Border;