Some mobile fixes.

Thanks to xet7 !

Fixes #5899
This commit is contained in:
Lauri Ojansivu 2025-10-08 23:32:13 +03:00
parent 05762aa50c
commit 3fda90612d
9 changed files with 259 additions and 46 deletions

View file

@ -33,6 +33,17 @@
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked { .board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none; 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) { @media screen and (max-width: 800px) {
.board-wrapper .board-canvas .swimlane { .board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;

View file

@ -16,13 +16,14 @@ template(name="boardBody")
if notDisplayThisBoard if notDisplayThisBoard
| {{_ 'tableVisibilityMode-allowPrivateOnly'}} | {{_ 'tableVisibilityMode-allowPrivateOnly'}}
else else
.board-wrapper(class=currentBoard.colorClass) .board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes( .board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if hasSwimlanes}}dragscroll{{/if}}"
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}" class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}" class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive.get}}is-dragging-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 if showOverlay.get
.board-overlay .board-overlay
if currentBoard.isTemplatesBoard if currentBoard.isTemplatesBoard

View file

@ -62,15 +62,11 @@ template(name="boardHeaderBar")
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" 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'}}") title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span
= currentBoard.stars
a.board-header-btn( a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}") title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn.js-watch-board( a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}") title="{{_ watchLevel }}")
@ -80,10 +76,8 @@ template(name="boardHeaderBar")
i.fa.fa-bell i.fa.fa-bell
if $eq watchLevel "muted" if $eq watchLevel "muted"
i.fa.fa-bell-slash i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}") a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin i.fa.fa-times-thin
@ -92,13 +86,11 @@ template(name="boardHeaderBar")
a.board-header-btn.js-log-in( a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}") title="{{_ 'log-in'}}")
i.fa.fa-sign-in i.fa.fa-sign-in
span {{_ 'log-in'}}
if isSandstorm if isSandstorm
if currentUser if currentUser
a.board-header-btn.js-open-archived-board a.board-header-btn.js-open-archived-board
i.fa.fa-archive i.fa.fa-archive
span {{_ 'archives'}}
//if showSort //if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}") // 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}}" title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}") class="{{#if Filter.isActive}}emphasis{{/if}}")
i.fa.fa-filter i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
if Filter.isActive if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}") a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin i.fa.fa-times-thin
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}") a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search i.fa.fa-search
span {{_ 'search'}}
unless currentBoard.isTemplatesBoard unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view( a.board-header-btn.js-toggle-board-view(
@ -128,14 +118,12 @@ template(name="boardHeaderBar")
i.fa.fa-trello i.fa.fa-trello
if $eq boardView 'board-view-cal' if $eq boardView 'board-view-cal'
i.fa.fa-calendar i.fa.fa-calendar
span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
if canModifyBoard if canModifyBoard
a.board-header-btn.js-multiselection-activate( a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}" title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}") class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o i.fa.fa-check-square-o
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}") a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin i.fa.fa-times-thin

View file

@ -192,6 +192,32 @@
font-size: 25px; font-size: 25px;
color: #fff; 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) { @media screen and (max-width: 800px) {
.board-list { .board-list {
height: 100%; height: 100%;
@ -218,6 +244,19 @@
font-size: 24px; 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) { @media screen and (max-width: 360px) {
li { li {
width: 100%; width: 100%;

View file

@ -29,7 +29,7 @@ template(name="boardList")
input#filterBtn(type="button" value="{{_ 'filter'}}") input#filterBtn(type="button" value="{{_ 'filter'}}")
input#resetBtn(type="button" value="{{_ 'filter-clear'}}") 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 li.js-add-board
a.board-list-item.label(title="{{_ 'add-board'}}") a.board-list-item.label(title="{{_ 'add-board'}}")
| {{_ 'add-board'}} | {{_ 'add-board'}}

View file

@ -214,6 +214,125 @@
#js-list-width-edit .list-width-error { #js-list-width-edit .list-width-error {
display: none; 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) { @media screen and (max-width: 800px) {
.mini-list { .mini-list {
flex: 0 0 60px; flex: 0 0 60px;
@ -267,24 +386,29 @@
left: -7px; left: -7px;
} }
.list-header .list-header-menu-icon { .list-header .list-header-menu-icon {
padding: 14px;
font-size: 40px;
text-align: center;
/* iOS Safari fallback positioning */
position: absolute; position: absolute;
padding: 7px; right: 60px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
right: 47px;
font-size: 20px;
} }
.list-header .list-header-handle { .list-header .list-header-handle {
padding: 14px;
font-size: 48px;
text-align: center;
/* iOS Safari fallback positioning */
position: absolute; position: absolute;
padding: 7px; right: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
right: 10px;
font-size: 24px;
} }
.list-header { .list-header {
display: grid; display: grid;
grid-template-columns: 30px 5fr 1fr; grid-template-columns: 30px 1fr auto auto;
gap: 10px;
} }
.list-header .list-header-left-icon { .list-header .list-header-left-icon {
display: grid; display: grid;
@ -295,16 +419,30 @@
grid-row: 1; grid-row: 1;
grid-column: 2; grid-column: 2;
align-self: end; align-self: end;
font-size: 20px;
font-weight: bold;
line-height: 1.2;
padding-bottom: 2px;
} }
.list-header .cardCount { .list-header .cardCount {
grid-row: 2; grid-row: 2;
grid-column: 2; grid-column: 2;
align-self: start; align-self: start;
font-size: 16px;
line-height: 1.2;
} }
.list-header .list-header-menu { .list-header .list-header-menu {
grid-row: 1/3; grid-row: 1/3;
grid-column: 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 { .list-header .inlined-form {
grid-row: 1/3; grid-row: 1/3;
grid-column: 1/4; grid-column: 1/4;

View file

@ -1,10 +1,10 @@
template(name='list') template(name='list')
.list.js-list(id="js-list-{{_id}}" .list.js-list(id="js-list-{{_id}}"
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}" 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 +listHeader
+listBody +listBody
template(name='miniList') 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 +listHeader

View file

@ -23,12 +23,12 @@ template(name="header")
a(href="{{pathFor 'board' id=_id slug=slug}}") a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer +viewer
= title = title
a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") //a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
i.fa.fa-arrows // i.fa.fa-arrows
if isShowDesktopDragHandles // if isShowDesktopDragHandles
i.fa.fa-check-square-o // i.fa.fa-check-square-o
unless isShowDesktopDragHandles // unless isShowDesktopDragHandles
i.fa.fa-ban // i.fa.fa-ban
#header-new-board-icon #header-new-board-icon
else else
//- //-
@ -61,12 +61,12 @@ template(name="header")
= title = title
else else
li.current.empty {{_ 'quick-access-description'}} li.current.empty {{_ 'quick-access-description'}}
a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}") //a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
i.fa.fa-arrows // i.fa.fa-arrows
if isShowDesktopDragHandles // if isShowDesktopDragHandles
i.fa.fa-check-square-o // i.fa.fa-check-square-o
unless isShowDesktopDragHandles // unless isShowDesktopDragHandles
i.fa.fa-ban // i.fa.fa-ban
// Next line is used only for spacing at header, // Next line is used only for spacing at header,
// there is no visible clickable icon. // there is no visible clickable icon.
#header-new-board-icon #header-new-board-icon

View file

@ -287,22 +287,50 @@ Utils = {
}, },
windowResizeDep: new Tracker.Dependency(), windowResizeDep: new Tracker.Dependency(),
// in fact, what we really care is screen size // 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 // 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. // in a small window (even on desktop), Wekan run in compact mode.
// we can easily debug with a small window of desktop browser. :-) // we can easily debug with a small window of desktop browser. :-)
isMiniScreen() { isMiniScreen() {
// OLD WINDOW WIDTH DETECTION:
this.windowResizeDep.depend(); 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() { isTouchScreen() {
// NEW TOUCH DEVICE DETECTION: // NEW TOUCH DEVICE DETECTION:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
var hasTouchScreen = false; var hasTouchScreen = false;
if ("maxTouchPoints" in navigator) { if ("maxTouchPoints" in navigator) {
hasTouchScreen = navigator.maxTouchPoints > 0; hasTouchScreen = navigator.maxTouchPoints > 0;
@ -328,12 +356,19 @@ Utils = {
// returns if desktop drag handles are enabled // returns if desktop drag handles are enabled
isShowDesktopDragHandles() { isShowDesktopDragHandles() {
//const currentUser = ReactiveCache.getCurrentUser(); if (this.isTouchScreen()) {
//if (currentUser) { return true;
// return (currentUser.profile || {}).showDesktopDragHandles; /*
//} else if (window.localStorage.getItem('showDesktopDragHandles')) { const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
//
if (window.localStorage.getItem('showDesktopDragHandles')) { if (window.localStorage.getItem('showDesktopDragHandles')) {
return true; return true;
} else {
return false;
*/
} else { } else {
return false; return false;
} }
@ -341,8 +376,9 @@ Utils = {
// returns if mini screen or desktop drag handles // returns if mini screen or desktop drag handles
isTouchScreenOrShowDesktopDragHandles() { isTouchScreenOrShowDesktopDragHandles() {
return this.isTouchScreen();
//return this.isTouchScreen() || this.isShowDesktopDragHandles(); //return this.isTouchScreen() || this.isShowDesktopDragHandles();
return this.isShowDesktopDragHandles(); //return this.isShowDesktopDragHandles();
}, },
calculateIndexData(prevData, nextData, nItems = 1) { calculateIndexData(prevData, nextData, nItems = 1) {