diff --git a/client/components/boards/boardBody.css b/client/components/boards/boardBody.css index 1138aa19a..197d71ddb 100644 --- a/client/components/boards/boardBody.css +++ b/client/components/boards/boardBody.css @@ -33,6 +33,17 @@ .board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked { display: none; } +/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ +.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane { + border-bottom: 1px solid #ccc; + display: flex; + flex-direction: column; + margin: 0; + padding: 0 0px 0px 0; + overflow-x: hidden; + overflow-y: auto; +} + @media screen and (max-width: 800px) { .board-wrapper .board-canvas .swimlane { border-bottom: 1px solid #ccc; diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index de1c1ca3c..057c4bc49 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -16,13 +16,14 @@ template(name="boardBody") if notDisplayThisBoard | {{_ 'tableVisibilityMode-allowPrivateOnly'}} else - .board-wrapper(class=currentBoard.colorClass) + .board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}") .board-canvas.js-swimlanes( class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}" class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}" class="{{#if draggingActive.get}}is-dragging-active{{/if}}" - class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}") + class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}" + class="{{#if isMiniScreen}}mobile-view{{/if}}") if showOverlay.get .board-overlay if currentBoard.isTemplatesBoard diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index db7d3886a..ef6738fa1 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -62,15 +62,11 @@ template(name="boardHeaderBar") a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") - if showStarCounter - span - = currentBoard.stars a.board-header-btn( class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" title="{{_ currentBoard.permission}}") i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") - span {{_ currentBoard.permission}} a.board-header-btn.js-watch-board( title="{{_ watchLevel }}") @@ -80,10 +76,8 @@ template(name="boardHeaderBar") i.fa.fa-bell if $eq watchLevel "muted" i.fa.fa-bell-slash - span {{_ watchLevel}} a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") i.fa.fa-sort - span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}} if isSortActive a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}") i.fa.fa-times-thin @@ -92,13 +86,11 @@ template(name="boardHeaderBar") a.board-header-btn.js-log-in( title="{{_ 'log-in'}}") i.fa.fa-sign-in - span {{_ 'log-in'}} if isSandstorm if currentUser a.board-header-btn.js-open-archived-board i.fa.fa-archive - span {{_ 'archives'}} //if showSort // a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}") @@ -109,14 +101,12 @@ template(name="boardHeaderBar") title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}" class="{{#if Filter.isActive}}emphasis{{/if}}") i.fa.fa-filter - span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}} if Filter.isActive a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}") i.fa.fa-times-thin a.board-header-btn.js-open-search-view(title="{{_ 'search'}}") i.fa.fa-search - span {{_ 'search'}} unless currentBoard.isTemplatesBoard a.board-header-btn.js-toggle-board-view( @@ -128,14 +118,12 @@ template(name="boardHeaderBar") i.fa.fa-trello if $eq boardView 'board-view-cal' i.fa.fa-calendar - span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}} if canModifyBoard a.board-header-btn.js-multiselection-activate( title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}" class="{{#if MultiSelection.isActive}}emphasis{{/if}}") i.fa.fa-check-square-o - span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}} if MultiSelection.isActive a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}") i.fa.fa-times-thin diff --git a/client/components/boards/boardsList.css b/client/components/boards/boardsList.css index 86bc42ac7..eeba4d11c 100644 --- a/client/components/boards/boardsList.css +++ b/client/components/boards/boardsList.css @@ -192,6 +192,32 @@ font-size: 25px; color: #fff; } +/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ +.board-list.mobile-view { + height: 100%; + overflow: scroll; +} +.board-list.mobile-view li { + width: 50%; +} +.board-list.mobile-view .board-list-item { + overflow: hidden; + height: 8rem; +} +.board-list.mobile-view .board-list-item-sub-name { + position: relative; + top: -100px; + left: -100px; +} +.board-list.mobile-view .board-handle { + position: absolute; + padding: 7px; + top: 50%; + transform: translateY(-50%); + right: 10px; + font-size: 24px; +} + @media screen and (max-width: 800px) { .board-list { height: 100%; @@ -218,6 +244,19 @@ font-size: 24px; } } +/* Mobile view styles for very small screens - applied when isMiniScreen is true */ +.board-list.mobile-view li { + width: 50%; +} +.board-list.mobile-view .board-handle { + position: absolute; + padding: 7px; + top: 50%; + transform: translateY(-50%); + right: 10px; + font-size: 24px; +} + @media screen and (max-width: 360px) { li { width: 100%; diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index d5d1aebbe..cd2fe9b8a 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -29,7 +29,7 @@ template(name="boardList") input#filterBtn(type="button" value="{{_ 'filter'}}") input#resetBtn(type="button" value="{{_ 'filter-clear'}}") - ul.board-list.clearfix.js-boards + ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}") li.js-add-board a.board-list-item.label(title="{{_ 'add-board'}}") | {{_ 'add-board'}} diff --git a/client/components/lists/list.css b/client/components/lists/list.css index b94a87f24..d8a3dee69 100644 --- a/client/components/lists/list.css +++ b/client/components/lists/list.css @@ -214,6 +214,125 @@ #js-list-width-edit .list-width-error { display: none; } +/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ +.mini-list.mobile-view { + flex: 0 0 60px; + height: auto; + width: 100%; + border-left: 0px; + border-bottom: 1px solid #ccc; +} +.list.mobile-view { + display: contents; + flex-basis: auto; + width: 100%; + border-left: 0px; +} +.list.mobile-view:first-child { + margin-left: 0px; +} +.list.mobile-view.ui-sortable-helper { + flex: 0 0 60px; + height: 60px; + width: 100%; + border-left: 0px; + border-bottom: 1px solid #ccc; +} +.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle { + cursor: grabbing; +} +.list.mobile-view.placeholder { + flex: 0 0 60px; + height: 60px; + width: 100%; + border-left: 0px; + border-bottom: 1px solid #ccc; +} +.list.mobile-view .list-body { + padding: 15px 19px; +} +.list.mobile-view .list-header { + /*Updated padding values for mobile devices, this should fix text grouping issue*/ + padding: 20px 0px 20px 0px; + border-bottom: 0px solid #e4e4e4; + min-height: 30px; + margin-top: 10px; + align-items: center; + /* Force grid layout for iPhone */ + display: grid !important; + grid-template-columns: 30px 1fr auto auto !important; + gap: 10px !important; +} +.list.mobile-view .list-header .list-header-left-icon { + padding: 7px; + padding-right: 27px; + margin-top: 1px; + top: -7px; + left: -7px; +} +.list.mobile-view .list-header .list-header-menu-icon { + padding: 14px; + font-size: 40px !important; + text-align: center; + /* Force positioning for iPhone */ + position: absolute !important; + right: 60px !important; + top: 50% !important; + transform: translateY(-50%) !important; + z-index: 10; +} +.list.mobile-view .list-header .list-header-handle { + padding: 14px; + font-size: 48px !important; + text-align: center; + /* Force positioning for iPhone */ + position: absolute !important; + right: 10px !important; + top: 50% !important; + transform: translateY(-50%) !important; + z-index: 10; +} +.list.mobile-view .list-header .list-header-left-icon { + display: grid; + grid-row: 1/3; + grid-column: 1; +} +.list.mobile-view .list-header .list-header-name { + grid-row: 1; + grid-column: 2; + align-self: end; + font-size: 20px !important; + font-weight: bold; + line-height: 1.2; + padding-bottom: 2px; +} +.list.mobile-view .list-header .cardCount { + grid-row: 2; + grid-column: 2; + align-self: start; + font-size: 16px !important; + line-height: 1.2; +} +.list.mobile-view .list-header .list-header-menu { + grid-row: 1/3; + grid-column: 3; +} +.list.mobile-view .list-header .list-header-menu-icon { + grid-row: 1/3; + grid-column: 3; +} +.list.mobile-view .list-header .list-header-handle { + grid-row: 1/3; + grid-column: 4; +} +.list.mobile-view .list-header .inlined-form { + grid-row: 1/3; + grid-column: 1/4; +} +.list.mobile-view .list-header .edit-controls { + align-items: initial; +} + @media screen and (max-width: 800px) { .mini-list { flex: 0 0 60px; @@ -267,24 +386,29 @@ left: -7px; } .list-header .list-header-menu-icon { + padding: 14px; + font-size: 40px; + text-align: center; + /* iOS Safari fallback positioning */ position: absolute; - padding: 7px; + right: 60px; top: 50%; transform: translateY(-50%); - right: 47px; - font-size: 20px; } .list-header .list-header-handle { + padding: 14px; + font-size: 48px; + text-align: center; + /* iOS Safari fallback positioning */ position: absolute; - padding: 7px; + right: 10px; top: 50%; transform: translateY(-50%); - right: 10px; - font-size: 24px; } .list-header { display: grid; - grid-template-columns: 30px 5fr 1fr; + grid-template-columns: 30px 1fr auto auto; + gap: 10px; } .list-header .list-header-left-icon { display: grid; @@ -295,16 +419,30 @@ grid-row: 1; grid-column: 2; align-self: end; + font-size: 20px; + font-weight: bold; + line-height: 1.2; + padding-bottom: 2px; } .list-header .cardCount { grid-row: 2; grid-column: 2; align-self: start; + font-size: 16px; + line-height: 1.2; } .list-header .list-header-menu { grid-row: 1/3; grid-column: 3; } + .list-header .list-header-menu-icon { + grid-row: 1/3; + grid-column: 3; + } + .list-header .list-header-handle { + grid-row: 1/3; + grid-column: 4; + } .list-header .inlined-form { grid-row: 1/3; grid-column: 1/4; diff --git a/client/components/lists/list.jade b/client/components/lists/list.jade index e39efcad0..0c87fd117 100644 --- a/client/components/lists/list.jade +++ b/client/components/lists/list.jade @@ -1,10 +1,10 @@ template(name='list') .list.js-list(id="js-list-{{_id}}" style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}" - class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}}") + class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}") +listHeader +listBody template(name='miniList') - a.mini-list.js-select-list.js-list(id="js-list-{{_id}}") + a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}") +listHeader diff --git a/client/components/main/header.jade b/client/components/main/header.jade index 6c8fe3dce..b9e61dea9 100644 --- a/client/components/main/header.jade +++ b/client/components/main/header.jade @@ -23,12 +23,12 @@ template(name="header") a(href="{{pathFor 'board' id=_id slug=slug}}") +viewer = title - a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") - i.fa.fa-arrows - if isShowDesktopDragHandles - i.fa.fa-check-square-o - unless isShowDesktopDragHandles - i.fa.fa-ban + //a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") + // i.fa.fa-arrows + // if isShowDesktopDragHandles + // i.fa.fa-check-square-o + // unless isShowDesktopDragHandles + // i.fa.fa-ban #header-new-board-icon else //- @@ -61,12 +61,12 @@ template(name="header") = title else li.current.empty {{_ 'quick-access-description'}} - a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") - i.fa.fa-arrows - if isShowDesktopDragHandles - i.fa.fa-check-square-o - unless isShowDesktopDragHandles - i.fa.fa-ban + //a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") + // i.fa.fa-arrows + // if isShowDesktopDragHandles + // i.fa.fa-check-square-o + // unless isShowDesktopDragHandles + // i.fa.fa-ban // Next line is used only for spacing at header, // there is no visible clickable icon. #header-new-board-icon diff --git a/client/lib/utils.js b/client/lib/utils.js index 15896e5c3..487961ab4 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -287,22 +287,50 @@ Utils = { }, 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() { - // OLD WINDOW WIDTH DETECTION: this.windowResizeDep.depend(); - return $(window).width() <= 800; + // 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; @@ -328,12 +356,19 @@ Utils = { // returns if desktop drag handles are enabled isShowDesktopDragHandles() { - //const currentUser = ReactiveCache.getCurrentUser(); - //if (currentUser) { - // return (currentUser.profile || {}).showDesktopDragHandles; - //} else if (window.localStorage.getItem('showDesktopDragHandles')) { + 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; } @@ -341,8 +376,9 @@ Utils = { // returns if mini screen or desktop drag handles isTouchScreenOrShowDesktopDragHandles() { + return this.isTouchScreen(); //return this.isTouchScreen() || this.isShowDesktopDragHandles(); - return this.isShowDesktopDragHandles(); + //return this.isShowDesktopDragHandles(); }, calculateIndexData(prevData, nextData, nItems = 1) {