utils.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. Utils = {
  2. /** returns the current board id
  3. * <li> returns the current board id or the board id of the popup card if set
  4. */
  5. getCurrentBoardId() {
  6. let popupCardBoardId = Session.get('popupCardBoardId');
  7. let currentBoard = Session.get('currentBoard');
  8. let ret = currentBoard;
  9. if (popupCardBoardId) {
  10. ret = popupCardBoardId;
  11. }
  12. return ret;
  13. },
  14. getCurrentCardId(ignorePopupCard) {
  15. let ret = Session.get('currentCard');
  16. if (!ret && !ignorePopupCard) {
  17. ret = Utils.getPopupCardId();
  18. }
  19. return ret;
  20. },
  21. getPopupCardId() {
  22. const ret = Session.get('popupCardId');
  23. return ret;
  24. },
  25. /** returns the current board
  26. * <li> returns the current board or the board of the popup card if set
  27. */
  28. getCurrentBoard() {
  29. const boardId = Utils.getCurrentBoardId();
  30. const ret = Boards.findOne(boardId);
  31. return ret;
  32. },
  33. getCurrentCard(ignorePopupCard) {
  34. const cardId = Utils.getCurrentCardId(ignorePopupCard);
  35. const ret = Cards.findOne(cardId);
  36. return ret;
  37. },
  38. getPopupCard() {
  39. const cardId = Utils.getPopupCardId();
  40. const ret = Cards.findOne(cardId);
  41. return ret;
  42. },
  43. reload () {
  44. // we move all window.location.reload calls into this function
  45. // so we can disable it when running tests.
  46. // This is because we are not allowed to override location.reload but
  47. // we can override Utils.reload to prevent reload during tests.
  48. window.location.reload();
  49. },
  50. setBoardView(view) {
  51. currentUser = Meteor.user();
  52. if (currentUser) {
  53. Meteor.user().setBoardView(view);
  54. } else if (view === 'board-view-swimlanes') {
  55. window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
  56. Utils.reload();
  57. } else if (view === 'board-view-lists') {
  58. window.localStorage.setItem('boardView', 'board-view-lists'); //true
  59. Utils.reload();
  60. } else if (view === 'board-view-cal') {
  61. window.localStorage.setItem('boardView', 'board-view-cal'); //true
  62. Utils.reload();
  63. } else {
  64. window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
  65. Utils.reload();
  66. }
  67. },
  68. unsetBoardView() {
  69. window.localStorage.removeItem('boardView');
  70. window.localStorage.removeItem('collapseSwimlane');
  71. },
  72. boardView() {
  73. currentUser = Meteor.user();
  74. if (currentUser) {
  75. return (currentUser.profile || {}).boardView;
  76. } else if (
  77. window.localStorage.getItem('boardView') === 'board-view-swimlanes'
  78. ) {
  79. return 'board-view-swimlanes';
  80. } else if (
  81. window.localStorage.getItem('boardView') === 'board-view-lists'
  82. ) {
  83. return 'board-view-lists';
  84. } else if (window.localStorage.getItem('boardView') === 'board-view-cal') {
  85. return 'board-view-cal';
  86. } else {
  87. window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
  88. Utils.reload();
  89. return 'board-view-swimlanes';
  90. }
  91. },
  92. myCardsSort() {
  93. let sort = window.localStorage.getItem('myCardsSort');
  94. if (!sort || !['board', 'dueAt'].includes(sort)) {
  95. sort = 'board';
  96. }
  97. return sort;
  98. },
  99. myCardsSortToggle() {
  100. if (this.myCardsSort() === 'board') {
  101. this.setMyCardsSort('dueAt');
  102. } else {
  103. this.setMyCardsSort('board');
  104. }
  105. },
  106. setMyCardsSort(sort) {
  107. window.localStorage.setItem('myCardsSort', sort);
  108. Utils.reload();
  109. },
  110. archivedBoardIds() {
  111. const archivedBoards = [];
  112. Boards.find({ archived: false }).forEach(board => {
  113. archivedBoards.push(board._id);
  114. });
  115. return archivedBoards;
  116. },
  117. dueCardsView() {
  118. let view = window.localStorage.getItem('dueCardsView');
  119. if (!view || !['me', 'all'].includes(view)) {
  120. view = 'me';
  121. }
  122. return view;
  123. },
  124. setDueCardsView(view) {
  125. window.localStorage.setItem('dueCardsView', view);
  126. Utils.reload();
  127. },
  128. // XXX We should remove these two methods
  129. goBoardId(_id) {
  130. const board = Boards.findOne(_id);
  131. return (
  132. board &&
  133. FlowRouter.go('board', {
  134. id: board._id,
  135. slug: board.slug,
  136. })
  137. );
  138. },
  139. goCardId(_id) {
  140. const card = Cards.findOne(_id);
  141. const board = Boards.findOne(card.boardId);
  142. return (
  143. board &&
  144. FlowRouter.go('card', {
  145. cardId: card._id,
  146. boardId: board._id,
  147. slug: board.slug,
  148. })
  149. );
  150. },
  151. MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
  152. COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
  153. addCommonMetaToAttachment(card, file) {
  154. if (card.isLinkedCard()) {
  155. file.boardId = Cards.findOne(card.linkedId).boardId;
  156. file.cardId = card.linkedId;
  157. } else {
  158. file.boardId = card.boardId;
  159. file.swimlaneId = card.swimlaneId;
  160. file.listId = card.listId;
  161. file.cardId = card._id;
  162. }
  163. file.userId = Meteor.userId();
  164. if (file.original) {
  165. file.original.name = file.name;
  166. }
  167. },
  168. shrinkImage(options) {
  169. // shrink image to certain size
  170. const dataurl = options.dataurl,
  171. callback = options.callback,
  172. toBlob = options.toBlob;
  173. let canvas = document.createElement('canvas'),
  174. image = document.createElement('img');
  175. const maxSize = options.maxSize || 1024;
  176. const ratio = options.ratio || 1.0;
  177. const next = function(result) {
  178. image = null;
  179. canvas = null;
  180. if (typeof callback === 'function') {
  181. callback(result);
  182. }
  183. };
  184. image.onload = function() {
  185. let width = this.width,
  186. height = this.height;
  187. let changed = false;
  188. if (width > height) {
  189. if (width > maxSize) {
  190. height *= maxSize / width;
  191. width = maxSize;
  192. changed = true;
  193. }
  194. } else if (height > maxSize) {
  195. width *= maxSize / height;
  196. height = maxSize;
  197. changed = true;
  198. }
  199. canvas.width = width;
  200. canvas.height = height;
  201. canvas.getContext('2d').drawImage(this, 0, 0, width, height);
  202. if (changed === true) {
  203. const type = 'image/jpeg';
  204. if (toBlob) {
  205. canvas.toBlob(next, type, ratio);
  206. } else {
  207. next(canvas.toDataURL(type, ratio));
  208. }
  209. } else {
  210. next(changed);
  211. }
  212. };
  213. image.onerror = function() {
  214. next(false);
  215. };
  216. image.src = dataurl;
  217. },
  218. capitalize(string) {
  219. return string.charAt(0).toUpperCase() + string.slice(1);
  220. },
  221. windowResizeDep: new Tracker.Dependency(),
  222. // in fact, what we really care is screen size
  223. // large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop
  224. // in a small window (even on desktop), Wekan run in compact mode.
  225. // we can easily debug with a small window of desktop browser. :-)
  226. isMiniScreen() {
  227. // OLD WINDOW WIDTH DETECTION:
  228. this.windowResizeDep.depend();
  229. return $(window).width() <= 800;
  230. // NEW TOUCH DEVICE DETECTION:
  231. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
  232. /*
  233. var hasTouchScreen = false;
  234. if ("maxTouchPoints" in navigator) {
  235. hasTouchScreen = navigator.maxTouchPoints > 0;
  236. } else if ("msMaxTouchPoints" in navigator) {
  237. hasTouchScreen = navigator.msMaxTouchPoints > 0;
  238. } else {
  239. var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
  240. if (mQ && mQ.media === "(pointer:coarse)") {
  241. hasTouchScreen = !!mQ.matches;
  242. } else if ('orientation' in window) {
  243. hasTouchScreen = true; // deprecated, but good fallback
  244. } else {
  245. // Only as a last resort, fall back to user agent sniffing
  246. var UA = navigator.userAgent;
  247. hasTouchScreen = (
  248. /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
  249. /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
  250. );
  251. }
  252. }
  253. */
  254. //if (hasTouchScreen)
  255. // document.getElementById("exampleButton").style.padding="1em";
  256. //return false;
  257. },
  258. // returns if desktop drag handles are enabled
  259. isShowDesktopDragHandles() {
  260. const currentUser = Meteor.user();
  261. if (currentUser) {
  262. return (currentUser.profile || {}).showDesktopDragHandles;
  263. } else if (window.localStorage.getItem('showDesktopDragHandles')) {
  264. return true;
  265. } else {
  266. return false;
  267. }
  268. },
  269. // returns if mini screen or desktop drag handles
  270. isMiniScreenOrShowDesktopDragHandles() {
  271. return this.isMiniScreen() || this.isShowDesktopDragHandles();
  272. },
  273. calculateIndexData(prevData, nextData, nItems = 1) {
  274. let base, increment;
  275. // If we drop the card to an empty column
  276. if (!prevData && !nextData) {
  277. base = 0;
  278. increment = 1;
  279. // If we drop the card in the first position
  280. } else if (!prevData) {
  281. base = nextData.sort - 1;
  282. increment = -1;
  283. // If we drop the card in the last position
  284. } else if (!nextData) {
  285. base = prevData.sort + 1;
  286. increment = 1;
  287. }
  288. // In the general case take the average of the previous and next element
  289. // sort indexes.
  290. else {
  291. const prevSortIndex = prevData.sort;
  292. const nextSortIndex = nextData.sort;
  293. increment = (nextSortIndex - prevSortIndex) / (nItems + 1);
  294. base = prevSortIndex + increment;
  295. }
  296. // XXX Return a generator that yield values instead of a base with a
  297. // increment number.
  298. return {
  299. base,
  300. increment,
  301. };
  302. },
  303. // Determine the new sort index
  304. calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
  305. let base, increment;
  306. // If we drop the card to an empty column
  307. if (!prevCardDomElement && !nextCardDomElement) {
  308. base = 0;
  309. increment = 1;
  310. // If we drop the card in the first position
  311. } else if (!prevCardDomElement) {
  312. base = Blaze.getData(nextCardDomElement).sort - 1;
  313. increment = -1;
  314. // If we drop the card in the last position
  315. } else if (!nextCardDomElement) {
  316. base = Blaze.getData(prevCardDomElement).sort + 1;
  317. increment = 1;
  318. }
  319. // In the general case take the average of the previous and next element
  320. // sort indexes.
  321. else {
  322. const prevSortIndex = Blaze.getData(prevCardDomElement).sort;
  323. const nextSortIndex = Blaze.getData(nextCardDomElement).sort;
  324. increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
  325. base = prevSortIndex + increment;
  326. }
  327. // XXX Return a generator that yield values instead of a base with a
  328. // increment number.
  329. return {
  330. base,
  331. increment,
  332. };
  333. },
  334. manageCustomUI() {
  335. Meteor.call('getCustomUI', (err, data) => {
  336. if (err && err.error[0] === 'var-not-exist') {
  337. Session.set('customUI', false); // siteId || address server not defined
  338. }
  339. if (!err) {
  340. Utils.setCustomUI(data);
  341. }
  342. });
  343. },
  344. setCustomUI(data) {
  345. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  346. if (currentBoard) {
  347. DocHead.setTitle(`${currentBoard.title} - ${data.productName}`);
  348. } else {
  349. DocHead.setTitle(`${data.productName}`);
  350. }
  351. },
  352. setMatomo(data) {
  353. window._paq = window._paq || [];
  354. window._paq.push(['setDoNotTrack', data.doNotTrack]);
  355. if (data.withUserName) {
  356. window._paq.push(['setUserId', Meteor.user().username]);
  357. }
  358. window._paq.push(['trackPageView']);
  359. window._paq.push(['enableLinkTracking']);
  360. (function() {
  361. window._paq.push(['setTrackerUrl', `${data.address}piwik.php`]);
  362. window._paq.push(['setSiteId', data.siteId]);
  363. const script = document.createElement('script');
  364. Object.assign(script, {
  365. id: 'scriptMatomo',
  366. type: 'text/javascript',
  367. async: 'true',
  368. defer: 'true',
  369. src: `${data.address}piwik.js`,
  370. });
  371. const s = document.getElementsByTagName('script')[0];
  372. s.parentNode.insertBefore(script, s);
  373. })();
  374. Session.set('matomo', true);
  375. },
  376. manageMatomo() {
  377. const matomo = Session.get('matomo');
  378. if (matomo === undefined) {
  379. Meteor.call('getMatomoConf', (err, data) => {
  380. if (err && err.error[0] === 'var-not-exist') {
  381. Session.set('matomo', false); // siteId || address server not defined
  382. }
  383. if (!err) {
  384. Utils.setMatomo(data);
  385. }
  386. });
  387. } else if (matomo) {
  388. window._paq.push(['trackPageView']);
  389. }
  390. },
  391. getTriggerActionDesc(event, tempInstance) {
  392. const jqueryEl = tempInstance.$(event.currentTarget.parentNode);
  393. const triggerEls = jqueryEl.find('.trigger-content').children();
  394. let finalString = '';
  395. for (let i = 0; i < triggerEls.length; i++) {
  396. const element = tempInstance.$(triggerEls[i]);
  397. if (element.hasClass('trigger-text')) {
  398. finalString += element.text().toLowerCase();
  399. } else if (element.hasClass('user-details')) {
  400. let username = element.find('input').val();
  401. if (username === undefined || username === '') {
  402. username = '*';
  403. }
  404. finalString += `${element
  405. .find('.trigger-text')
  406. .text()
  407. .toLowerCase()} ${username}`;
  408. } else if (element.find('select').length > 0) {
  409. finalString += element
  410. .find('select option:selected')
  411. .text()
  412. .toLowerCase();
  413. } else if (element.find('input').length > 0) {
  414. let inputvalue = element.find('input').val();
  415. if (inputvalue === undefined || inputvalue === '') {
  416. inputvalue = '*';
  417. }
  418. finalString += inputvalue;
  419. }
  420. // Add space
  421. if (i !== length - 1) {
  422. finalString += ' ';
  423. }
  424. }
  425. return finalString;
  426. },
  427. fallbackCopyTextToClipboard(text) {
  428. var textArea = document.createElement("textarea");
  429. textArea.value = text;
  430. // Avoid scrolling to bottom
  431. textArea.style.top = "0";
  432. textArea.style.left = "0";
  433. textArea.style.position = "fixed";
  434. document.body.appendChild(textArea);
  435. textArea.focus();
  436. textArea.select();
  437. try {
  438. document.execCommand('copy');
  439. return Promise.resolve(true);
  440. } catch (e) {
  441. return Promise.reject(false);
  442. } finally {
  443. document.body.removeChild(textArea);
  444. }
  445. },
  446. /** copy the text to the clipboard
  447. * @see https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript/30810322#30810322
  448. * @param string copy this text to the clipboard
  449. * @return Promise
  450. */
  451. copyTextToClipboard(text) {
  452. let ret;
  453. if (navigator.clipboard) {
  454. ret = navigator.clipboard.writeText(text).then(function() {
  455. }, function(err) {
  456. console.error('Async: Could not copy text: ', err);
  457. });
  458. } else {
  459. ret = Utils.fallbackCopyTextToClipboard(text);
  460. }
  461. return ret;
  462. },
  463. /** show the "copied!" message
  464. * @param promise the promise of Utils.copyTextToClipboard
  465. * @param $tooltip jQuery tooltip element
  466. */
  467. showCopied(promise, $tooltip) {
  468. if (promise) {
  469. promise.then(() => {
  470. $tooltip.show(100);
  471. setTimeout(() => $tooltip.hide(100), 1000);
  472. }, (err) => {
  473. console.error("error: ", err);
  474. });
  475. }
  476. },
  477. };
  478. // A simple tracker dependency that we invalidate every time the window is
  479. // resized. This is used to reactively re-calculate the popup position in case
  480. // of a window resize. This is the equivalent of a "Signal" in some other
  481. // programming environments (eg, elm).
  482. $(window).on('resize', () => Utils.windowResizeDep.changed());