import { ReactiveCache } from '/imports/reactiveCache'; Utils = { setBackgroundImage(url) { const currentBoard = Utils.getCurrentBoard(); if (currentBoard.backgroundImageURL !== undefined) { $(".board-wrapper").css({"background":"url(" + currentBoard.backgroundImageURL + ")","background-size":"cover"}); $(".swimlane,.swimlane .list,.swimlane .list .list-body,.swimlane .list:first-child .list-body").css({"background-color":"transparent"}); $(".minicard").css({"opacity": "0.9"}); } else if (currentBoard["background-color"]) { currentBoard.setColor(currentBoard["background-color"]); } }, /** returns the current board id *
  • returns the current board id or the board id of the popup card if set */ getCurrentBoardId() { let popupCardBoardId = Session.get('popupCardBoardId'); let currentBoard = Session.get('currentBoard'); let ret = currentBoard; if (popupCardBoardId) { ret = popupCardBoardId; } return ret; }, getCurrentCardId(ignorePopupCard) { let ret = Session.get('currentCard'); if (!ret && !ignorePopupCard) { ret = Utils.getPopupCardId(); } return ret; }, getPopupCardId() { const ret = Session.get('popupCardId'); return ret; }, getCurrentListId() { const ret = Session.get('currentList'); return ret; }, /** returns the current board *
  • returns the current board or the board of the popup card if set */ getCurrentBoard() { const boardId = Utils.getCurrentBoardId(); const ret = ReactiveCache.getBoard(boardId); return ret; }, getCurrentCard(ignorePopupCard) { const cardId = Utils.getCurrentCardId(ignorePopupCard); const ret = ReactiveCache.getCard(cardId); return ret; }, // Zoom and mobile mode utilities getZoomLevel() { const user = ReactiveCache.getCurrentUser(); if (user && user.profile && user.profile.zoomLevel !== undefined) { return user.profile.zoomLevel; } // For non-logged-in users, check localStorage const stored = localStorage.getItem('wekan-zoom-level'); return stored ? parseFloat(stored) : 1.0; }, setZoomLevel(level) { const user = ReactiveCache.getCurrentUser(); if (user) { // Update user profile user.setZoomLevel(level); } else { // Store in localStorage for non-logged-in users localStorage.setItem('wekan-zoom-level', level.toString()); } Utils.applyZoomLevel(level); // Trigger reactive updates for UI components Session.set('wekan-zoom-level', level); }, getMobileMode() { const user = ReactiveCache.getCurrentUser(); if (user && user.profile && user.profile.mobileMode !== undefined) { return user.profile.mobileMode; } // For non-logged-in users, check localStorage const stored = localStorage.getItem('wekan-mobile-mode'); return stored ? stored === 'true' : false; }, setMobileMode(enabled) { const user = ReactiveCache.getCurrentUser(); if (user) { // Update user profile user.setMobileMode(enabled); } else { // Store in localStorage for non-logged-in users localStorage.setItem('wekan-mobile-mode', enabled.toString()); } Utils.applyMobileMode(enabled); // Trigger reactive updates for UI components Session.set('wekan-mobile-mode', enabled); }, applyZoomLevel(level) { const boardWrapper = document.querySelector('.board-wrapper'); const body = document.body; const isMobileMode = body.classList.contains('mobile-mode'); if (boardWrapper) { if (isMobileMode) { // On mobile mode, only apply zoom to text and icons, not the entire layout // Remove any existing transform from board-wrapper boardWrapper.style.transform = ''; boardWrapper.style.transformOrigin = ''; // Apply zoom to text and icon elements instead const textElements = boardWrapper.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, .minicard, .list-header-name, .board-header-btn, .fa, .icon'); textElements.forEach(element => { element.style.transform = `scale(${level})`; element.style.transformOrigin = 'center'; }); // Reset board-canvas height const boardCanvas = document.querySelector('.board-canvas'); if (boardCanvas) { boardCanvas.style.height = ''; } } else { // Desktop mode: apply zoom to entire board-wrapper as before boardWrapper.style.transform = `scale(${level})`; boardWrapper.style.transformOrigin = 'top left'; // If zoom is 50% or lower, make board wrapper full width like content if (level <= 0.5) { boardWrapper.style.width = '100%'; boardWrapper.style.maxWidth = '100%'; boardWrapper.style.margin = '0'; } else { // Reset to normal width for higher zoom levels boardWrapper.style.width = ''; boardWrapper.style.maxWidth = ''; boardWrapper.style.margin = ''; } // Adjust container height to prevent scroll issues const boardCanvas = document.querySelector('.board-canvas'); if (boardCanvas) { boardCanvas.style.height = `${100 / level}%`; // For high zoom levels (200%+), enable both horizontal and vertical scrolling if (level >= 2.0) { boardCanvas.style.overflowX = 'auto'; boardCanvas.style.overflowY = 'auto'; // Ensure the content area can scroll both horizontally and vertically const content = document.querySelector('#content'); if (content) { content.style.overflowX = 'auto'; content.style.overflowY = 'auto'; } } else { // Reset overflow for normal zoom levels boardCanvas.style.overflowX = ''; boardCanvas.style.overflowY = ''; const content = document.querySelector('#content'); if (content) { content.style.overflowX = ''; content.style.overflowY = ''; } } } } } }, applyMobileMode(enabled) { const body = document.body; if (enabled) { body.classList.add('mobile-mode'); body.classList.remove('desktop-mode'); } else { body.classList.add('desktop-mode'); body.classList.remove('mobile-mode'); } }, initializeUserSettings() { // Apply saved settings on page load const zoomLevel = Utils.getZoomLevel(); const mobileMode = Utils.getMobileMode(); Utils.applyZoomLevel(zoomLevel); Utils.applyMobileMode(mobileMode); }, getCurrentList() { const listId = this.getCurrentListId(); let ret = null; if (listId) { ret = ReactiveCache.getList(listId); } return ret; }, getPopupCard() { const cardId = Utils.getPopupCardId(); const ret = ReactiveCache.getCard(cardId); return ret; }, canModifyCard() { const currentUser = ReactiveCache.getCurrentUser(); const ret = ( currentUser && currentUser.isBoardMember() && !currentUser.isCommentOnly() && !currentUser.isWorker() ); return ret; }, canModifyBoard() { const currentUser = ReactiveCache.getCurrentUser(); const ret = ( currentUser && currentUser.isBoardMember() && !currentUser.isCommentOnly() ); return ret; }, reload() { // we move all window.location.reload calls into this function // so we can disable it when running tests. // This is because we are not allowed to override location.reload but // we can override Utils.reload to prevent reload during tests. window.location.reload(); }, setBoardView(view) { currentUser = ReactiveCache.getCurrentUser(); if (currentUser) { ReactiveCache.getCurrentUser().setBoardView(view); } else if (view === 'board-view-swimlanes') { window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true Utils.reload(); } else if (view === 'board-view-lists') { window.localStorage.setItem('boardView', 'board-view-lists'); //true Utils.reload(); } else if (view === 'board-view-cal') { window.localStorage.setItem('boardView', 'board-view-cal'); //true Utils.reload(); } else { window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true Utils.reload(); } }, unsetBoardView() { window.localStorage.removeItem('boardView'); window.localStorage.removeItem('collapseSwimlane'); }, boardView() { currentUser = ReactiveCache.getCurrentUser(); if (currentUser) { return (currentUser.profile || {}).boardView; } else if ( window.localStorage.getItem('boardView') === 'board-view-swimlanes' ) { return 'board-view-swimlanes'; } else if ( window.localStorage.getItem('boardView') === 'board-view-lists' ) { return 'board-view-lists'; } else if (window.localStorage.getItem('boardView') === 'board-view-cal') { return 'board-view-cal'; } else { window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true Utils.reload(); return 'board-view-swimlanes'; } }, myCardsSort() { let sort = window.localStorage.getItem('myCardsSort'); if (!sort || !['board', 'dueAt'].includes(sort)) { sort = 'board'; } return sort; }, myCardsSortToggle() { if (this.myCardsSort() === 'board') { this.setMyCardsSort('dueAt'); } else { this.setMyCardsSort('board'); } }, setMyCardsSort(sort) { window.localStorage.setItem('myCardsSort', sort); Utils.reload(); }, archivedBoardIds() { const ret = ReactiveCache.getBoards({ archived: false }).map(board => board._id); return ret; }, dueCardsView() { let view = window.localStorage.getItem('dueCardsView'); if (!view || !['me', 'all'].includes(view)) { view = 'me'; } return view; }, setDueCardsView(view) { window.localStorage.setItem('dueCardsView', view); Utils.reload(); }, myCardsView() { let view = window.localStorage.getItem('myCardsView'); if (!view || !['boards', 'table'].includes(view)) { view = 'boards'; } return view; }, setMyCardsView(view) { window.localStorage.setItem('myCardsView', view); Utils.reload(); }, // XXX We should remove these two methods goBoardId(_id) { const board = ReactiveCache.getBoard(_id); return ( board && FlowRouter.go('board', { id: board._id, slug: board.slug, }) ); }, goCardId(_id) { const card = ReactiveCache.getCard(_id); const board = ReactiveCache.getBoard(card.boardId); return ( board && FlowRouter.go('card', { cardId: card._id, boardId: board._id, slug: board.slug, }) ); }, getCommonAttachmentMetaFrom(card) { const meta = {}; if (card.isLinkedCard()) { meta.boardId = ReactiveCache.getCard(card.linkedId).boardId; meta.cardId = card.linkedId; } else { meta.boardId = card.boardId; meta.swimlaneId = card.swimlaneId; meta.listId = card.listId; meta.cardId = card._id; } return meta; }, MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL, COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO, shrinkImage(options) { // shrink image to certain size const dataurl = options.dataurl, callback = options.callback, toBlob = options.toBlob; let canvas = document.createElement('canvas'), image = document.createElement('img'); const maxSize = options.maxSize || 1024; const ratio = options.ratio || 1.0; const next = function (result) { image = null; canvas = null; if (typeof callback === 'function') { callback(result); } }; image.onload = function () { let width = this.width, height = this.height; let changed = false; if (width > height) { if (width > maxSize) { height *= maxSize / width; width = maxSize; changed = true; } } else if (height > maxSize) { width *= maxSize / height; height = maxSize; changed = true; } canvas.width = width; canvas.height = height; canvas.getContext('2d').drawImage(this, 0, 0, width, height); if (changed === true) { const type = 'image/jpeg'; if (toBlob) { canvas.toBlob(next, type, ratio); } else { next(canvas.toDataURL(type, ratio)); } } else { next(changed); } }; image.onerror = function () { next(false); }; image.src = dataurl; }, capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); }, windowResizeDep: new Tracker.Dependency(), // in fact, what we really care is screen size // large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop // in a small window (even on desktop), Wekan run in compact mode. // we can easily debug with a small window of desktop browser. :-) isMiniScreen() { this.windowResizeDep.depend(); // Show mobile view when: // 1. Screen width is 800px or less (matches CSS media queries) // 2. Mobile phones in portrait mode // 3. iPad in very small screens (≤ 600px) const isSmallScreen = window.innerWidth <= 800; const isVerySmallScreen = window.innerWidth <= 600; const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches; const isMobilePhone = /Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && !/iPad/i.test(navigator.userAgent); const isIPhone = /iPhone|iPod/i.test(navigator.userAgent); const isIPad = /iPad/i.test(navigator.userAgent); const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent); // For iPhone: always show mobile view regardless of orientation // For other mobile phones: show mobile view in portrait, desktop view in landscape // For iPad: show mobile view only in very small screens (≤ 600px) // For Ubuntu Touch: smartphones behave like mobile phones, tablets like iPad // For desktop: show mobile view when screen width <= 800px if (isIPhone) { return true; // iPhone: always mobile view } else if (isMobilePhone) { return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop } else if (isIPad) { return isVerySmallScreen; // iPad: only very small screens get mobile view } else if (isUbuntuTouch) { // Ubuntu Touch: smartphones (≤ 600px) behave like mobile phones, tablets (> 600px) like iPad if (isVerySmallScreen) { return isPortrait; // Ubuntu Touch smartphone: portrait = mobile, landscape = desktop } else { return isVerySmallScreen; // Ubuntu Touch tablet: only very small screens get mobile view } } else { return isSmallScreen; // Desktop: based on 800px screen width } }, isTouchScreen() { // NEW TOUCH DEVICE DETECTION: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent var hasTouchScreen = false; if ("maxTouchPoints" in navigator) { hasTouchScreen = navigator.maxTouchPoints > 0; } else if ("msMaxTouchPoints" in navigator) { hasTouchScreen = navigator.msMaxTouchPoints > 0; } else { var mQ = window.matchMedia && matchMedia("(pointer:coarse)"); if (mQ && mQ.media === "(pointer:coarse)") { hasTouchScreen = !!mQ.matches; } else if ('orientation' in window) { hasTouchScreen = true; // deprecated, but good fallback } else { // Only as a last resort, fall back to user agent sniffing var UA = navigator.userAgent; hasTouchScreen = ( /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA) ); } } return hasTouchScreen; }, // returns if desktop drag handles are enabled isShowDesktopDragHandles() { if (this.isTouchScreen()) { return true; /* const currentUser = ReactiveCache.getCurrentUser(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; } else if (window.localStorage.getItem('showDesktopDragHandles')) { // if (window.localStorage.getItem('showDesktopDragHandles')) { return true; } else { return false; */ } else { return false; } }, // returns if mini screen or desktop drag handles isTouchScreenOrShowDesktopDragHandles() { // Always enable drag handles for mobile screens (touch devices) return this.isTouchScreen() || this.isMiniScreen(); //return this.isTouchScreen() || this.isShowDesktopDragHandles(); //return this.isShowDesktopDragHandles(); }, calculateIndexData(prevData, nextData, nItems = 1) { let base, increment; // If we drop the card to an empty column if (!prevData && !nextData) { base = 0; increment = 1; // If we drop the card in the first position } else if (!prevData) { const nextSortIndex = nextData.sort; const ceil = Math.ceil(nextSortIndex - 1); if (ceil < nextSortIndex) { increment = nextSortIndex - ceil; base = nextSortIndex - increment; } else { base = nextData.sort - 1; increment = -1; } // If we drop the card in the last position } else if (!nextData) { const prevSortIndex = prevData.sort; const floor = Math.floor(prevSortIndex + 1); if (floor > prevSortIndex) { increment = prevSortIndex - floor; base = prevSortIndex - increment; } else { base = prevData.sort + 1; increment = 1; } } // In the general case take the average of the previous and next element // sort indexes. else { const prevSortIndex = prevData.sort; const nextSortIndex = nextData.sort; if (nItems == 1 ) { if (prevSortIndex < 0 ) { const ceil = Math.ceil(nextSortIndex - 1); if (ceil < nextSortIndex && ceil > prevSortIndex) { increment = ceil - prevSortIndex; } } else { const floor = Math.floor(nextSortIndex - 1); if (floor < nextSortIndex && floor > prevSortIndex) { increment = floor - prevSortIndex; } } } if (!increment) { increment = (nextSortIndex - prevSortIndex) / (nItems + 1); } if (!base) { base = prevSortIndex + increment; } } // XXX Return a generator that yield values instead of a base with a // increment number. return { base, increment, }; }, // Determine the new sort index calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) { let prevData = null; let nextData = null; if (prevCardDomElement) { prevData = Blaze.getData(prevCardDomElement) } if (nextCardDomElement) { nextData = Blaze.getData(nextCardDomElement); } const ret = Utils.calculateIndexData(prevData, nextData, nCards); return ret; }, manageCustomUI() { Meteor.call('getCustomUI', (err, data) => { if (err && err.error[0] === 'var-not-exist') { Session.set('customUI', false); // siteId || address server not defined } if (!err) { Utils.setCustomUI(data); } }); }, setCustomUI(data) { const currentBoard = Utils.getCurrentBoard(); if (currentBoard) { DocHead.setTitle(`${currentBoard.title} - ${data.productName}`); } else { DocHead.setTitle(`${data.productName}`); } }, setMatomo(data) { window._paq = window._paq || []; window._paq.push(['setDoNotTrack', data.doNotTrack]); if (data.withUserName) { window._paq.push(['setUserId', ReactiveCache.getCurrentUser().username]); } window._paq.push(['trackPageView']); window._paq.push(['enableLinkTracking']); (function () { window._paq.push(['setTrackerUrl', `${data.address}piwik.php`]); window._paq.push(['setSiteId', data.siteId]); const script = document.createElement('script'); Object.assign(script, { id: 'scriptMatomo', type: 'text/javascript', async: 'true', defer: 'true', src: `${data.address}piwik.js`, }); const s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(script, s); })(); Session.set('matomo', true); }, manageMatomo() { const matomo = Session.get('matomo'); if (matomo === undefined) { Meteor.call('getMatomoConf', (err, data) => { if (err && err.error[0] === 'var-not-exist') { Session.set('matomo', false); // siteId || address server not defined } if (!err) { Utils.setMatomo(data); } }); } else if (matomo) { window._paq.push(['trackPageView']); } }, getTriggerActionDesc(event, tempInstance) { const jqueryEl = tempInstance.$(event.currentTarget.parentNode); const triggerEls = jqueryEl.find('.trigger-content').children(); let finalString = ''; for (let i = 0; i < triggerEls.length; i++) { const element = tempInstance.$(triggerEls[i]); if (element.hasClass('trigger-text')) { finalString += element.text().toLowerCase(); } else if (element.hasClass('user-details')) { let username = element.find('input').val(); if (username === undefined || username === '') { username = '*'; } finalString += `${element .find('.trigger-text') .text() .toLowerCase()} ${username}`; } else if (element.find('select').length > 0) { finalString += element .find('select option:selected') .text() .toLowerCase(); } else if (element.find('input').length > 0) { let inputvalue = element.find('input').val(); if (inputvalue === undefined || inputvalue === '') { inputvalue = '*'; } finalString += inputvalue; } // Add space if (i !== length - 1) { finalString += ' '; } } return finalString; }, fallbackCopyTextToClipboard(text) { var textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); return Promise.resolve(true); } catch (e) { return Promise.reject(false); } finally { document.body.removeChild(textArea); } }, /** copy the text to the clipboard * @see https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript/30810322#30810322 * @param string copy this text to the clipboard * @return Promise */ copyTextToClipboard(text) { let ret; if (navigator.clipboard) { ret = navigator.clipboard.writeText(text).then(function () { }, function (err) { console.error('Async: Could not copy text: ', err); }); } else { ret = Utils.fallbackCopyTextToClipboard(text); } return ret; }, /** show the "copied!" message * @param promise the promise of Utils.copyTextToClipboard * @param $tooltip jQuery tooltip element */ showCopied(promise, $tooltip) { if (promise) { promise.then(() => { $tooltip.show(100); setTimeout(() => $tooltip.hide(100), 1000); }, (err) => { console.error("error: ", err); }); } }, }; // A simple tracker dependency that we invalidate every time the window is // resized. This is used to reactively re-calculate the popup position in case // of a window resize. This is the equivalent of a "Signal" in some other // programming environments (eg, elm). $(window).on('resize', () => Utils.windowResizeDep.changed());