| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 | 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   * <li> 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   * <li> 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();    // Also depend on mobile mode changes to make this reactive    Session.get('wekan-mobile-mode');        // 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)    // 4. All iPhone models by default (including largest models), but respect user preference    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);    // Check if user has explicitly set mobile mode preference    const userMobileMode = this.getMobileMode();        // For iPhone: default to mobile view, but respect user's mobile mode toggle preference    // This ensures all iPhone models (including iPhone 15 Pro Max, 14 Pro Max, etc.) start with mobile view    // but users can still switch to desktop mode if they prefer    if (isIPhone) {      // If user has explicitly set a preference, respect it      if (userMobileMode !== null && userMobileMode !== undefined) {        return userMobileMode;      }      // Otherwise, default to mobile view for iPhones      return true;    } 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() {    // Always show drag handles on all displays    return true;  },  // returns if mini screen or desktop drag handles  isTouchScreenOrShowDesktopDragHandles() {    // Always enable drag handles for all displays    return true;  },  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());
 |