utils.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. getCommonAttachmentMetaFrom(card) {
  152. let meta;
  153. if (card.isLinkedCard()) {
  154. meta.boardId = Cards.findOne(card.linkedId).boardId;
  155. meta.cardId = card.linkedId;
  156. } else {
  157. meta.boardId = card.boardId;
  158. meta.swimlaneId = card.swimlaneId;
  159. meta.listId = card.listId;
  160. meta.cardId = card._id;
  161. }
  162. return meta;
  163. },
  164. MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
  165. COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
  166. shrinkImage(options) {
  167. // shrink image to certain size
  168. const dataurl = options.dataurl,
  169. callback = options.callback,
  170. toBlob = options.toBlob;
  171. let canvas = document.createElement('canvas'),
  172. image = document.createElement('img');
  173. const maxSize = options.maxSize || 1024;
  174. const ratio = options.ratio || 1.0;
  175. const next = function(result) {
  176. image = null;
  177. canvas = null;
  178. if (typeof callback === 'function') {
  179. callback(result);
  180. }
  181. };
  182. image.onload = function() {
  183. let width = this.width,
  184. height = this.height;
  185. let changed = false;
  186. if (width > height) {
  187. if (width > maxSize) {
  188. height *= maxSize / width;
  189. width = maxSize;
  190. changed = true;
  191. }
  192. } else if (height > maxSize) {
  193. width *= maxSize / height;
  194. height = maxSize;
  195. changed = true;
  196. }
  197. canvas.width = width;
  198. canvas.height = height;
  199. canvas.getContext('2d').drawImage(this, 0, 0, width, height);
  200. if (changed === true) {
  201. const type = 'image/jpeg';
  202. if (toBlob) {
  203. canvas.toBlob(next, type, ratio);
  204. } else {
  205. next(canvas.toDataURL(type, ratio));
  206. }
  207. } else {
  208. next(changed);
  209. }
  210. };
  211. image.onerror = function() {
  212. next(false);
  213. };
  214. image.src = dataurl;
  215. },
  216. capitalize(string) {
  217. return string.charAt(0).toUpperCase() + string.slice(1);
  218. },
  219. windowResizeDep: new Tracker.Dependency(),
  220. // in fact, what we really care is screen size
  221. // large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop
  222. // in a small window (even on desktop), Wekan run in compact mode.
  223. // we can easily debug with a small window of desktop browser. :-)
  224. isMiniScreen() {
  225. // OLD WINDOW WIDTH DETECTION:
  226. this.windowResizeDep.depend();
  227. return $(window).width() <= 800;
  228. // NEW TOUCH DEVICE DETECTION:
  229. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
  230. /*
  231. var hasTouchScreen = false;
  232. if ("maxTouchPoints" in navigator) {
  233. hasTouchScreen = navigator.maxTouchPoints > 0;
  234. } else if ("msMaxTouchPoints" in navigator) {
  235. hasTouchScreen = navigator.msMaxTouchPoints > 0;
  236. } else {
  237. var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
  238. if (mQ && mQ.media === "(pointer:coarse)") {
  239. hasTouchScreen = !!mQ.matches;
  240. } else if ('orientation' in window) {
  241. hasTouchScreen = true; // deprecated, but good fallback
  242. } else {
  243. // Only as a last resort, fall back to user agent sniffing
  244. var UA = navigator.userAgent;
  245. hasTouchScreen = (
  246. /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
  247. /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
  248. );
  249. }
  250. }
  251. */
  252. //if (hasTouchScreen)
  253. // document.getElementById("exampleButton").style.padding="1em";
  254. //return false;
  255. },
  256. // returns if desktop drag handles are enabled
  257. isShowDesktopDragHandles() {
  258. const currentUser = Meteor.user();
  259. if (currentUser) {
  260. return (currentUser.profile || {}).showDesktopDragHandles;
  261. } else if (window.localStorage.getItem('showDesktopDragHandles')) {
  262. return true;
  263. } else {
  264. return false;
  265. }
  266. },
  267. // returns if mini screen or desktop drag handles
  268. isMiniScreenOrShowDesktopDragHandles() {
  269. return this.isMiniScreen() || this.isShowDesktopDragHandles();
  270. },
  271. calculateIndexData(prevData, nextData, nItems = 1) {
  272. let base, increment;
  273. // If we drop the card to an empty column
  274. if (!prevData && !nextData) {
  275. base = 0;
  276. increment = 1;
  277. // If we drop the card in the first position
  278. } else if (!prevData) {
  279. base = nextData.sort - 1;
  280. increment = -1;
  281. // If we drop the card in the last position
  282. } else if (!nextData) {
  283. base = prevData.sort + 1;
  284. increment = 1;
  285. }
  286. // In the general case take the average of the previous and next element
  287. // sort indexes.
  288. else {
  289. const prevSortIndex = prevData.sort;
  290. const nextSortIndex = nextData.sort;
  291. increment = (nextSortIndex - prevSortIndex) / (nItems + 1);
  292. base = prevSortIndex + increment;
  293. }
  294. // XXX Return a generator that yield values instead of a base with a
  295. // increment number.
  296. return {
  297. base,
  298. increment,
  299. };
  300. },
  301. // Determine the new sort index
  302. calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
  303. let base, increment;
  304. // If we drop the card to an empty column
  305. if (!prevCardDomElement && !nextCardDomElement) {
  306. base = 0;
  307. increment = 1;
  308. // If we drop the card in the first position
  309. } else if (!prevCardDomElement) {
  310. base = Blaze.getData(nextCardDomElement).sort - 1;
  311. increment = -1;
  312. // If we drop the card in the last position
  313. } else if (!nextCardDomElement) {
  314. base = Blaze.getData(prevCardDomElement).sort + 1;
  315. increment = 1;
  316. }
  317. // In the general case take the average of the previous and next element
  318. // sort indexes.
  319. else {
  320. const prevSortIndex = Blaze.getData(prevCardDomElement).sort;
  321. const nextSortIndex = Blaze.getData(nextCardDomElement).sort;
  322. increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
  323. base = prevSortIndex + increment;
  324. }
  325. // XXX Return a generator that yield values instead of a base with a
  326. // increment number.
  327. return {
  328. base,
  329. increment,
  330. };
  331. },
  332. manageCustomUI() {
  333. Meteor.call('getCustomUI', (err, data) => {
  334. if (err && err.error[0] === 'var-not-exist') {
  335. Session.set('customUI', false); // siteId || address server not defined
  336. }
  337. if (!err) {
  338. Utils.setCustomUI(data);
  339. }
  340. });
  341. },
  342. setCustomUI(data) {
  343. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  344. if (currentBoard) {
  345. DocHead.setTitle(`${currentBoard.title} - ${data.productName}`);
  346. } else {
  347. DocHead.setTitle(`${data.productName}`);
  348. }
  349. },
  350. setMatomo(data) {
  351. window._paq = window._paq || [];
  352. window._paq.push(['setDoNotTrack', data.doNotTrack]);
  353. if (data.withUserName) {
  354. window._paq.push(['setUserId', Meteor.user().username]);
  355. }
  356. window._paq.push(['trackPageView']);
  357. window._paq.push(['enableLinkTracking']);
  358. (function() {
  359. window._paq.push(['setTrackerUrl', `${data.address}piwik.php`]);
  360. window._paq.push(['setSiteId', data.siteId]);
  361. const script = document.createElement('script');
  362. Object.assign(script, {
  363. id: 'scriptMatomo',
  364. type: 'text/javascript',
  365. async: 'true',
  366. defer: 'true',
  367. src: `${data.address}piwik.js`,
  368. });
  369. const s = document.getElementsByTagName('script')[0];
  370. s.parentNode.insertBefore(script, s);
  371. })();
  372. Session.set('matomo', true);
  373. },
  374. manageMatomo() {
  375. const matomo = Session.get('matomo');
  376. if (matomo === undefined) {
  377. Meteor.call('getMatomoConf', (err, data) => {
  378. if (err && err.error[0] === 'var-not-exist') {
  379. Session.set('matomo', false); // siteId || address server not defined
  380. }
  381. if (!err) {
  382. Utils.setMatomo(data);
  383. }
  384. });
  385. } else if (matomo) {
  386. window._paq.push(['trackPageView']);
  387. }
  388. },
  389. getTriggerActionDesc(event, tempInstance) {
  390. const jqueryEl = tempInstance.$(event.currentTarget.parentNode);
  391. const triggerEls = jqueryEl.find('.trigger-content').children();
  392. let finalString = '';
  393. for (let i = 0; i < triggerEls.length; i++) {
  394. const element = tempInstance.$(triggerEls[i]);
  395. if (element.hasClass('trigger-text')) {
  396. finalString += element.text().toLowerCase();
  397. } else if (element.hasClass('user-details')) {
  398. let username = element.find('input').val();
  399. if (username === undefined || username === '') {
  400. username = '*';
  401. }
  402. finalString += `${element
  403. .find('.trigger-text')
  404. .text()
  405. .toLowerCase()} ${username}`;
  406. } else if (element.find('select').length > 0) {
  407. finalString += element
  408. .find('select option:selected')
  409. .text()
  410. .toLowerCase();
  411. } else if (element.find('input').length > 0) {
  412. let inputvalue = element.find('input').val();
  413. if (inputvalue === undefined || inputvalue === '') {
  414. inputvalue = '*';
  415. }
  416. finalString += inputvalue;
  417. }
  418. // Add space
  419. if (i !== length - 1) {
  420. finalString += ' ';
  421. }
  422. }
  423. return finalString;
  424. },
  425. fallbackCopyTextToClipboard(text) {
  426. var textArea = document.createElement("textarea");
  427. textArea.value = text;
  428. // Avoid scrolling to bottom
  429. textArea.style.top = "0";
  430. textArea.style.left = "0";
  431. textArea.style.position = "fixed";
  432. document.body.appendChild(textArea);
  433. textArea.focus();
  434. textArea.select();
  435. try {
  436. document.execCommand('copy');
  437. return Promise.resolve(true);
  438. } catch (e) {
  439. return Promise.reject(false);
  440. } finally {
  441. document.body.removeChild(textArea);
  442. }
  443. },
  444. /** copy the text to the clipboard
  445. * @see https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript/30810322#30810322
  446. * @param string copy this text to the clipboard
  447. * @return Promise
  448. */
  449. copyTextToClipboard(text) {
  450. let ret;
  451. if (navigator.clipboard) {
  452. ret = navigator.clipboard.writeText(text).then(function() {
  453. }, function(err) {
  454. console.error('Async: Could not copy text: ', err);
  455. });
  456. } else {
  457. ret = Utils.fallbackCopyTextToClipboard(text);
  458. }
  459. return ret;
  460. },
  461. /** show the "copied!" message
  462. * @param promise the promise of Utils.copyTextToClipboard
  463. * @param $tooltip jQuery tooltip element
  464. */
  465. showCopied(promise, $tooltip) {
  466. if (promise) {
  467. promise.then(() => {
  468. $tooltip.show(100);
  469. setTimeout(() => $tooltip.hide(100), 1000);
  470. }, (err) => {
  471. console.error("error: ", err);
  472. });
  473. }
  474. },
  475. };
  476. // A simple tracker dependency that we invalidate every time the window is
  477. // resized. This is used to reactively re-calculate the popup position in case
  478. // of a window resize. This is the equivalent of a "Signal" in some other
  479. // programming environments (eg, elm).
  480. $(window).on('resize', () => Utils.windowResizeDep.changed());