mirror of
https://github.com/wekan/wekan.git
synced 2026-02-09 09:44:22 +01:00
Resolve merge conflicts by accepting PR #6131 changes
Co-authored-by: xet7 <15545+xet7@users.noreply.github.com>
This commit is contained in:
parent
dc0b68ee80
commit
97dd5d2064
257 changed files with 9483 additions and 14103 deletions
|
|
@ -1,6 +1,6 @@
|
|||
.my-cards-board-wrapper {
|
||||
border-radius: 0 0 0.5vw 0.5vw;
|
||||
min-width: min(400px, 52vw);
|
||||
min-width: min(100%, 400px, 52vw);
|
||||
margin-bottom: 2.5vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
|
@ -33,13 +33,6 @@
|
|||
text-align: center;
|
||||
margin-bottom: 0.9vh;
|
||||
}
|
||||
.my-cards-list-wrapper {
|
||||
margin: 1.3vh 1.3vw;
|
||||
border-radius: 0.7vw;
|
||||
display: inline-grid;
|
||||
min-width: min(250px, 32vw);
|
||||
max-width: min(350px, 45vw);
|
||||
}
|
||||
.my-cards-card-wrapper {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.3vh;
|
||||
|
|
@ -81,7 +74,7 @@
|
|||
}
|
||||
|
||||
.accessibility-page h2 {
|
||||
font-size: 24px;
|
||||
|
||||
margin-bottom: 20px;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
.new-comment a.fa.fa-brands.fa-markdown,
|
||||
.inlined-form a.fa.fa-brands.fa-markdown {
|
||||
float: right;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 60px;
|
||||
.new-comment, .inlined-form {
|
||||
a.fa.fa-brands.fa-markdown, a.fa.fa-copy {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
.new-comment a.fa.fa-copy,
|
||||
.inlined-form a.fa.fa-copy {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
right: 5px;
|
||||
}
|
||||
.js-inlined-form.viewer.btn-sm {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 6px;
|
||||
.editor-controls {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
grid-area: editor-controls;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
.editor {
|
||||
grid-area: editor;
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
template(name="editor")
|
||||
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
.editor-controls
|
||||
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip.copied-tooltip-hidden {{_ 'copied'}}
|
||||
textarea.editor(
|
||||
dir="auto"
|
||||
class="{{class}}"
|
||||
id=id
|
||||
autofocus=autofocus
|
||||
placeholder="{{_ 'comment-placeholder'}}")
|
||||
+Template.contentBlock
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ BlazeComponent.extendComponent({
|
|||
|
||||
const enableTextarea = function() {
|
||||
const $textarea = this.$(textareaSelector);
|
||||
autosize($textarea);
|
||||
$textarea.escapeableTextComplete(mentions);
|
||||
};
|
||||
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === true || Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.global-search-board-wrapper {
|
||||
border-radius: 8px;
|
||||
min-width: 400px;
|
||||
border-radius: 0.8ch;
|
||||
min-width: min(100%, 400px);
|
||||
border-width: 8px;
|
||||
border-color: #808080;
|
||||
border-style: solid;
|
||||
|
|
@ -67,8 +67,6 @@
|
|||
color: #8b0000;
|
||||
}
|
||||
.global-search-page {
|
||||
width: 40%;
|
||||
min-width: 400px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
line-height: 150%;
|
||||
|
|
@ -91,6 +89,13 @@
|
|||
font-family: Courier;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lists-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1ch 0.3lh;
|
||||
|
||||
}
|
||||
code {
|
||||
color: #000;
|
||||
background-color: #d3d3d3;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,100 +5,81 @@ template(name="header")
|
|||
Reddit "subreddit" bar.
|
||||
The first link goes to the boards page.
|
||||
if currentUser
|
||||
#header-quick-access(class=currentBoard.colorClass)
|
||||
#header-quick-access(class="currentBoard.colorClass {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
// Home icon - always at left side of logo
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
i.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
#header-quick-access-left
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-home
|
||||
span
|
||||
| {{_ 'all-boards'}}
|
||||
|
||||
// Logo - visible; on mobile constrained by CSS
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
|
||||
unless currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
unless currentSetting.customTopLeftCornerLogoImageUrl
|
||||
div#headerIsSettingDatabaseCallDone
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
|
||||
// Zoom controls - always visible
|
||||
.zoom-controls
|
||||
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
|
||||
span.zoom-display {{zoomLevel}}%
|
||||
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
|
||||
|
||||
// Drag handles toggle - between zoom and mobile mode toggle
|
||||
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
|
||||
i.fa.fa-arrows
|
||||
if isShowDesktopDragHandles
|
||||
i.fa.fa-check
|
||||
unless isShowDesktopDragHandles
|
||||
i.fa.fa-ban
|
||||
|
||||
if isMiniScreen
|
||||
ul.header-quick-access-list
|
||||
if currentList
|
||||
each currentBoard.lists
|
||||
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
|
||||
a.js-select-list
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
if isMiniScreen
|
||||
ul.header-quick-access-list
|
||||
if currentList
|
||||
each currentBoard.lists
|
||||
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
|
||||
a.js-select-list.
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
each currentUser.starredBoards
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
ul.header-quick-access-list
|
||||
//li
|
||||
// a(href="{{pathFor 'public'}}")
|
||||
// span.fa.fa-globe
|
||||
// | {{_ 'public'}}
|
||||
each currentUser.starredBoards
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
li.current.empty(title="{{_ 'quick-access-description'}}")
|
||||
| {{_ 'quick-access-description'}}
|
||||
#header-new-board-icon
|
||||
// Next line is used only for spacing at header,
|
||||
// there is no visible clickable icon.
|
||||
#header-new-board-icon
|
||||
else
|
||||
ul.header-quick-access-list
|
||||
//li
|
||||
// a(href="{{pathFor 'public'}}")
|
||||
// span.fa.fa-globe
|
||||
// | {{_ 'public'}}
|
||||
each currentUser.starredBoards
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
// Hide duplicate create board button,
|
||||
// because it did not show board templates correctly.
|
||||
//a#header-new-board-icon.js-create-board
|
||||
// i.fa.fa-plus(title="Create a new board")
|
||||
// Logo - visible; on mobile constrained by CSS
|
||||
unless currentSetting.hideLogo
|
||||
.logo-container
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a.logo(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
+logo
|
||||
else
|
||||
+logo
|
||||
else
|
||||
li.current.empty(title="{{_ 'quick-access-description'}}")
|
||||
| {{_ 'quick-access-description'}}
|
||||
#header-new-board-icon
|
||||
// Next line is used only for spacing at header,
|
||||
// there is no visible clickable icon.
|
||||
#header-new-board-icon
|
||||
// Hide duplicate create board button,
|
||||
// because it did not show board templates correctly.
|
||||
//a#header-new-board-icon.js-create-board
|
||||
// i.fa.fa-plus(title="Create a new board")
|
||||
div#headerIsSettingDatabaseCallDone.logo
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
|
||||
.mobile-mode-toggle
|
||||
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
|
||||
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
|
||||
i.fa.fa-mobile
|
||||
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
|
||||
i.fa.fa-desktop
|
||||
|
||||
// Notifications
|
||||
+notifications
|
||||
|
||||
if currentSetting.customHelpLinkUrl
|
||||
#header-help
|
||||
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
|
||||
i.fa.fa-question-circle
|
||||
|
||||
+headerUserBar
|
||||
#header-quick-access-right
|
||||
if currentSetting.customHelpLinkUrl
|
||||
#header-help
|
||||
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
|
||||
i.fa.fa-question-circle
|
||||
#header-quick-access-icons
|
||||
+headerUserBar
|
||||
// Notifications
|
||||
+notifications
|
||||
|
||||
#header(class=currentBoard.colorClass)
|
||||
//-
|
||||
The main bar is a colorful bar that provide all the meta-data for the
|
||||
current page. This bar is contextual based.
|
||||
If the user is not connected we display "sign in" and "log in" buttons.
|
||||
#header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
|
||||
#header-main-bar(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if wrappedHeader}}wrapper{{/if}}")
|
||||
+Template.dynamic(template=headerBar)
|
||||
|
||||
if appIsOffline
|
||||
|
|
@ -122,3 +103,7 @@ template(name="offlineWarning")
|
|||
| {{_ 'app-is-offline'}}
|
||||
|
||||
a.app-try-reconnect {{_ 'app-try-reconnect'}}
|
||||
|
||||
//- a little helper to avoid duplication
|
||||
template(name="logo")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" style="{{#if currentSetting.customTopLeftCornerLogoHeight}}min-height: #{currentSetting.customTopLeftCornerLogoHeight};{{/if}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
|
|
@ -22,13 +22,13 @@ Template.header.onCreated(function () {
|
|||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'none';
|
||||
).style.visibility = 'hidden';
|
||||
else if (
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'block';
|
||||
).style.visibility = 'visible';
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
|
|
@ -57,14 +57,6 @@ Template.header.helpers({
|
|||
return announcements && announcements.body;
|
||||
},
|
||||
|
||||
zoomLevel() {
|
||||
const sessionZoom = Session.get('wekan-zoom-level');
|
||||
if (sessionZoom !== undefined) {
|
||||
return Math.round(sessionZoom * 100);
|
||||
}
|
||||
return Math.round(Utils.getZoomLevel() * 100);
|
||||
},
|
||||
|
||||
mobileMode() {
|
||||
const sessionMode = Session.get('wekan-mobile-mode');
|
||||
if (sessionMode !== undefined) {
|
||||
|
|
@ -76,51 +68,6 @@ Template.header.helpers({
|
|||
|
||||
Template.header.events({
|
||||
'click .js-create-board': Popup.open('headerBarCreateBoard'),
|
||||
'click .js-zoom-level-click'(evt) {
|
||||
const $zoomDisplay = $(evt.currentTarget).find('.zoom-display');
|
||||
const $zoomInput = $(evt.currentTarget).find('.zoom-input');
|
||||
|
||||
// Hide display, show input
|
||||
$zoomDisplay.hide();
|
||||
$zoomInput.show().focus().select();
|
||||
},
|
||||
|
||||
'keypress .js-zoom-input'(evt) {
|
||||
if (evt.which === 13) {
|
||||
// Enter key
|
||||
const newZoomPercent = parseInt(evt.target.value);
|
||||
|
||||
if (
|
||||
!isNaN(newZoomPercent) &&
|
||||
newZoomPercent >= 50 &&
|
||||
newZoomPercent <= 300
|
||||
) {
|
||||
const newZoom = newZoomPercent / 100;
|
||||
Utils.setZoomLevel(newZoom);
|
||||
|
||||
// Hide input, show display
|
||||
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
|
||||
const $zoomInput = $(evt.target);
|
||||
$zoomInput.hide();
|
||||
$zoomDisplay.show();
|
||||
} else {
|
||||
alert('Please enter a zoom level between 50% and 300%');
|
||||
evt.target.focus().select();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'blur .js-zoom-input'(evt) {
|
||||
// When input loses focus, hide it and show display
|
||||
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
|
||||
const $zoomInput = $(evt.target);
|
||||
$zoomInput.hide();
|
||||
$zoomDisplay.show();
|
||||
},
|
||||
'click .js-mobile-mode-toggle'() {
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'click .js-open-bookmarks'(evt) {
|
||||
// Already added but ensure single definition -- safe guard
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
.shortcuts-list .shortcuts-list-item .shortcuts-list-item-keys kbd {
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
font-size: 18px;
|
||||
|
||||
}
|
||||
.shortcuts-list .shortcuts-list-item .shortcuts-list-item-action {
|
||||
font-size: 1.4em;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,33 @@
|
|||
* {
|
||||
-webkit-box-sizing: unset;
|
||||
box-sizing: unset;
|
||||
/* Global variables that we can use to easily test and change layout
|
||||
Later it could be useful to use a CSS superset */
|
||||
/* this makes the property computable */
|
||||
@property --popup-margin {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: 0px;
|
||||
}
|
||||
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
--label-height: 1.7lh;
|
||||
--header-scale: clamp(1rem, 1.333rem + -0.333vw, 1.3rem)
|
||||
--popup-margin: 2vmax;
|
||||
|
||||
/* regarding fonts, this is one of the clearest I found: https://modern-fluid-typography.vercel.app/ */
|
||||
&:has(body.desktop-mode) {
|
||||
font-size: clamp(1rem, 1.68rem + -0.57vw, 1.4rem);
|
||||
--quick-header-scale: clamp(0.8rem, 0.6rem + 0.4vw, 1.2rem);
|
||||
--list-item-size: 1.2em;
|
||||
}
|
||||
|
||||
&:has(body.mobile-mode) {
|
||||
font-size: clamp(2.5rem, 3vw + 1.7rem, 3.5rem);
|
||||
--quick-header-scale: 1.3em;
|
||||
--header-scale: clamp(1rem, -0.5vw + 1.25rem, 1.125rem);
|
||||
--list-item-size: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fixed missing 'import nib' stylesheet reset and extra li bullet points
|
||||
* https://github.com/wekan/wekan/issues/4512#issuecomment-1129347536
|
||||
*/
|
||||
|
|
@ -32,29 +58,26 @@ a:focus {
|
|||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: unset;
|
||||
min-width: unset;
|
||||
padding: unset;
|
||||
font-size: unset;
|
||||
font-weight: unset;
|
||||
line-height: unset;
|
||||
color: unset;
|
||||
text-align: unset;
|
||||
white-space: unset;
|
||||
vertical-align: unset;
|
||||
background-color: unset;
|
||||
border-radius: unset;
|
||||
display: flex;
|
||||
gap: 0 0.3ch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
body {
|
||||
/* changed programmatically on swimlane resizes, or e.g. when un-collapsed */
|
||||
transition: height 0.2s ease-out, width 0.2s ease-out;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
line-height: 1.4;
|
||||
color: #4d4d4d;
|
||||
font-family: Roboto, Poppins, "Helvetica Neue", "Liberation Sans", Arial, Helvetica, sans-serif;
|
||||
color: hsl(0, 0%, 30%);
|
||||
/* Improve text rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
|
@ -63,58 +86,74 @@ button {
|
|||
user-select: text;
|
||||
}
|
||||
html {
|
||||
font-size: 100%;
|
||||
max-height: 100%;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
overscroll-behavior: none;
|
||||
|
||||
}
|
||||
body {
|
||||
background: #dedede;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
/* iOS Safari fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
/* height is auto; if set to 100vh, it prevents navbar to disappear on scroll... */
|
||||
width: 100%;
|
||||
/* Needs to be set on body and html. Feels ok to disable entirely as Wekan is really drag/scroll-heavy */
|
||||
overscroll-behavior: none;
|
||||
min-height: 100vh;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Mobile mode specific fixes for iOS Safari */
|
||||
body.mobile-mode {
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/* Prevent iOS Safari bounce scroll */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Ensure content area is scrollable in mobile mode */
|
||||
body.mobile-mode #content {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
/* Prevent scroll through popups */
|
||||
body:has(.pop-over:hover) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Some forms will need extra adjustement (removing margins, etc)
|
||||
but it worth it to let browsers take care of exact placement/sizing */
|
||||
.inlined-form {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 0.3lh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 1vh;
|
||||
min-height: 100vh;
|
||||
max-width: min(100%, 100vw);
|
||||
}
|
||||
#content .sk-spinner {
|
||||
margin-top: 30vh;
|
||||
}
|
||||
#content > .wrapper {
|
||||
margin-top: 1vh;
|
||||
padding: 2vh 2vw;
|
||||
}
|
||||
#modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -157,25 +196,6 @@ body.mobile-mode #content {
|
|||
#modal .modal-content-wide .modal-close-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
line-height: 1.2em;
|
||||
margin: 0 0 1vh;
|
||||
}
|
||||
h2 {
|
||||
font-size: clamp(16px, 3.5vw, 20px);
|
||||
line-height: 1.2em;
|
||||
margin: 0 0 0.8vh;
|
||||
}
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.25em;
|
||||
margin: 0 0 0.6vh;
|
||||
}
|
||||
.quiet,
|
||||
.quiet a {
|
||||
|
|
@ -226,7 +246,7 @@ p {
|
|||
}
|
||||
p a {
|
||||
text-decoration: underline;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
table,
|
||||
p {
|
||||
|
|
@ -250,13 +270,13 @@ blockquote {
|
|||
padding: 0 0 0 1vw;
|
||||
}
|
||||
hr {
|
||||
height: 1px;
|
||||
height: 0.2ch;
|
||||
border: 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
background: #dbdbdb;
|
||||
color: #dbdbdb;
|
||||
margin: 2vh 0;
|
||||
margin: 0.2lh 0;
|
||||
padding: 0;
|
||||
}
|
||||
table,
|
||||
|
|
@ -303,7 +323,7 @@ kbd {
|
|||
clear: both;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
.show {
|
||||
display: block;
|
||||
|
|
@ -337,8 +357,11 @@ kbd {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
.wrapper {
|
||||
width: calc(100% - 2vw);
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
height: fit-content;
|
||||
display: grid;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
|
|
@ -369,8 +392,12 @@ kbd {
|
|||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.invisible-line {
|
||||
height: 1.3lh;
|
||||
visibility: hidden;
|
||||
}
|
||||
.wrapword {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.grab {
|
||||
cursor: grab;
|
||||
|
|
@ -445,8 +472,39 @@ a:not(.disabled).is-active i.fa {
|
|||
}
|
||||
.viewer {
|
||||
min-height: 2.5vh;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
/* a tentative to get layout less dependant of content,
|
||||
especially for small elements e.g. labels: the goal is that
|
||||
content will be cut with `...` if too large (but will be fully
|
||||
rendered in dedicated interfaces)
|
||||
|
||||
the classic technique is to use flex-basis, but it depends
|
||||
on the parent not overflowing to get the right size; also,
|
||||
specifying in terms of lines makes the browser act clever, by
|
||||
fitting the available space and cutting after N lines, whatever
|
||||
is the text's length */
|
||||
min-width: 0;
|
||||
p, ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
/* See https: //css-tricks.com/line-clampin/,
|
||||
it is widely supported and waiting standardization https: //caniuse.com/?search=-webkit-line-clamp */
|
||||
display: -webkit-box !important;
|
||||
/* 0 has no effect; ensures will not interfere unless asked */
|
||||
-webkit-line-clamp: var(--overflow-lines, 0);
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-align-items: center;
|
||||
/* grid properties apply */
|
||||
align-content: center;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
.viewer table {
|
||||
word-wrap: normal;
|
||||
|
|
@ -481,6 +539,12 @@ a:not(.disabled).is-active i.fa {
|
|||
padding: 0;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.basicTabs-container .tabs-list .tab-item {
|
||||
/* where does templates_tabs.css come from? visible in
|
||||
devtools but not in sources */
|
||||
font-size: unset !important;
|
||||
}
|
||||
.no-scrollbars {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
|
@ -495,133 +559,30 @@ a:not(.disabled).is-active i.fa {
|
|||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: portrait) {
|
||||
#content {
|
||||
margin: 1px 0px 0px 0px;
|
||||
height: calc(100% - 0px);
|
||||
/* Improve touch scrolling */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#content > .wrapper {
|
||||
margin-top: 0px;
|
||||
padding: 8px;
|
||||
}
|
||||
.wrapper {
|
||||
height: calc(100% - 31px);
|
||||
margin: 0px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
width: 95vw;
|
||||
max-width: 95vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/* Improve touch targets */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
/* Prevent zoom on iOS */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
||||
/* Form elements */
|
||||
input, select, textarea {
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
/* Prevent zoom on iOS */
|
||||
padding: 12px;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Cards and lists */
|
||||
.minicard {
|
||||
min-height: 48px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Board canvas */
|
||||
.board-canvas {
|
||||
padding: 0 8px 8px 0;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Header mobile layout */
|
||||
#header {
|
||||
padding: 8px;
|
||||
/* Keep top bar on a single row on small screens */
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#header-quick-access {
|
||||
/* Keep quick-access items in one row */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide elements that should move to the hamburger menu on mobile */
|
||||
#header-quick-access .header-quick-access-list,
|
||||
#header-quick-access #header-help {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only the home icon (hide the trailing text) on mobile */
|
||||
#header-quick-access .home-icon a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 28px; /* enough to display the icon */
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Hide text in home icon on mobile, show only icon */
|
||||
#header-quick-access .home-icon a span:not(.fa) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for mobile header elements */
|
||||
#header-quick-access .zoom-controls {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mobile-mode-toggle {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#header-user-bar {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Ensure header elements don't wrap on very small screens */
|
||||
#header-quick-access {
|
||||
min-width: 0; /* Allow flexbox to shrink */
|
||||
}
|
||||
|
||||
/* Make sure logo doesn't take too much space on mobile */
|
||||
#header-quick-access img {
|
||||
max-height: 24px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* Ensure zoom controls are compact on mobile */
|
||||
.zoom-controls .zoom-level {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Modal mobile optimization */
|
||||
#modal .modal-content,
|
||||
#modal .modal-content-wide {
|
||||
|
|
@ -632,29 +593,28 @@ a:not(.disabled).is-active i.fa {
|
|||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Table mobile optimization */
|
||||
table {
|
||||
font-size: 14px;
|
||||
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
|
||||
/* Admin panel mobile optimization */
|
||||
.setting-content .content-body {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .main-body {
|
||||
order: 1;
|
||||
min-height: 60vh;
|
||||
|
|
@ -663,139 +623,175 @@ a:not(.disabled).is-active i.fa {
|
|||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/* Tablet devices (768px - 1024px) */
|
||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||
#content > .wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.panel-default {
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
|
||||
/* Touch-friendly but more compact */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
|
||||
.minicard {
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.list {
|
||||
margin: 0 12px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
.board-canvas {
|
||||
padding: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
|
||||
#header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content {
|
||||
width: 80vw;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 90vw;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive handling for quick-access description on tablets */
|
||||
#header-quick-access ul.header-quick-access-list li.current.empty {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
||||||| parent of 2e0149f79 (🚧 Remove zoom/mobile option, rework header/misc layout to be more responsive)
|
||||
/* Tablet devices (768px - 1024px) */
|
||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||
#content > .wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
/* Touch-friendly but more compact */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.minicard {
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 12px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.board-canvas {
|
||||
padding: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
#modal .modal-content {
|
||||
width: 80vw;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 90vw;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
=======
|
||||
>>>>>>> 2e0149f79 (🚧 Remove zoom/mobile option, rework header/misc layout to be more responsive)
|
||||
|
||||
/* Large displays and digital signage (1920px+) */
|
||||
@media screen and (min-width: 1920px) {
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 56px;
|
||||
min-width: 56px;
|
||||
padding: 16px 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.minicard {
|
||||
min-height: 56px;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 8px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.board-canvas {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
|
||||
#content > .wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
.inline-input {
|
||||
height: 37px;
|
||||
margin: 8px 10px 0 0;
|
||||
width: 100px;
|
||||
|
||||
.ui-sortable-handle {
|
||||
cursor: grab !important;
|
||||
}
|
||||
|
||||
.select-authentication {
|
||||
width: 100%;
|
||||
}
|
||||
.textBelowCustomLoginLogo,
|
||||
.auth-layout {
|
||||
#rescue-card-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.auth-layout .auth-dialog {
|
||||
margin: 0 !important;
|
||||
flex: 1 0 auto;
|
||||
align-self: center;
|
||||
margin: 0 0.2lh;
|
||||
}
|
||||
.loadingText {
|
||||
text-align: center;
|
||||
|
|
@ -882,8 +878,18 @@ a:not(.disabled).is-active i.fa {
|
|||
text-decoration: underline;
|
||||
text-decoration-color: #17683a;
|
||||
}
|
||||
/*
|
||||
Prevents popups to compute real size, trying to comment
|
||||
.at-pwd-form, .at-sep, .at-oauth {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
#at-pwd-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: stretch;
|
||||
gap: 0.3lh;
|
||||
}
|
||||
@-moz-keyframes fadeIn {
|
||||
from {
|
||||
|
|
@ -928,31 +934,19 @@ a:not(.disabled).is-active i.fa {
|
|||
|
||||
/* iOS Safari Mobile Mode Fixes */
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Prevent scrolling issues on iOS Safari when card popup is open */
|
||||
body.mobile-mode {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Fix z-index stacking for mobile Safari */
|
||||
body.mobile-mode .board-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
|
||||
body.mobile-mode .pop-over {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure smooth scrolling on iOS */
|
||||
body.mobile-mode .card-details,
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
|
|
|
|||
|
|
@ -23,61 +23,56 @@ template(name="main")
|
|||
//link(rel="stylesheet" type="text/css" class="__meteor-css__" href="css/html5-default-theme.css")
|
||||
|
||||
template(name="userFormsLayout")
|
||||
section.auth-layout
|
||||
if currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
br
|
||||
br
|
||||
unless currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
if currentSetting.customLoginLogoImageUrl
|
||||
if currentSetting.customLoginLogoLinkUrl
|
||||
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}" width="300" height="auto")
|
||||
.auth-container
|
||||
section.auth-layout.auth-logo
|
||||
if currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customLoginLogoImageUrl
|
||||
if currentSetting.customLoginLogoLinkUrl
|
||||
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}")
|
||||
unless currentSetting.customLoginLogoLinkUrl
|
||||
a
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}")
|
||||
else
|
||||
a
|
||||
img(src="{{pathFor '/wekan-logo.svg'}}" alt="")
|
||||
br
|
||||
unless currentSetting.customLoginLogoLinkUrl
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}" width="300" height="auto")
|
||||
br
|
||||
else
|
||||
img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
|
||||
br
|
||||
if currentSetting.textBelowCustomLoginLogo
|
||||
hr
|
||||
section.textBelowCustomLoginLogo
|
||||
+viewer
|
||||
| {{currentSetting.textBelowCustomLoginLogo}}
|
||||
hr
|
||||
section.auth-layout
|
||||
section.auth-dialog
|
||||
if isLoading
|
||||
+loader
|
||||
else
|
||||
// ARIA live region for error messages
|
||||
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
|
||||
+Template.dynamic(template=content)
|
||||
if currentSetting.displayAuthenticationMethod
|
||||
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
|
||||
if isLegalNoticeLinkExist
|
||||
div#legalNoticeDiv
|
||||
span#legalNoticeSpan {{_ 'acceptance_of_our_legalNotice'}}
|
||||
a#legalNoticeAtLink.at-link(href="{{currentSetting.legalNotice}}", target="_blank", rel="noopener noreferrer")
|
||||
| {{_ 'legalNotice'}}
|
||||
if getLegalNoticeWithWritTraduction
|
||||
div
|
||||
div.at-form-lang
|
||||
label(for="userform-set-language-select") {{_ 'changeLanguagePopup-title'}}
|
||||
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
|
||||
each languages
|
||||
if isCurrentLanguage
|
||||
if rtl
|
||||
option(value="{{tag}}" selected="selected") {{name}} (RTL)
|
||||
section.auth-custom-text
|
||||
if currentSetting.textBelowCustomLoginLogo
|
||||
section.textBelowCustomLoginLogo
|
||||
+viewer
|
||||
| {{currentSetting.textBelowCustomLoginLogo}}
|
||||
section.auth-layout.auth-form
|
||||
section.auth-dialog
|
||||
if isLoading
|
||||
+loader
|
||||
else
|
||||
// ARIA live region for error messages
|
||||
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f;")
|
||||
+Template.dynamic(template=content)
|
||||
if currentSetting.displayAuthenticationMethod
|
||||
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
|
||||
if isLegalNoticeLinkExist
|
||||
div#legalNoticeDiv
|
||||
span#legalNoticeSpan {{_ 'acceptance_of_our_legalNotice'}}
|
||||
a#legalNoticeAtLink.at-link(href="{{currentSetting.legalNotice}}", target="_blank", rel="noopener noreferrer")
|
||||
| {{_ 'legalNotice'}}
|
||||
div.at-form-lang
|
||||
label(for="userform-set-language-select") {{_ 'changeLanguagePopup-title'}}
|
||||
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
|
||||
each languages
|
||||
if isCurrentLanguage
|
||||
if rtl
|
||||
option(value="{{tag}}" selected="selected") {{name}} (RTL)
|
||||
else
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
else
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
else
|
||||
if rtl
|
||||
option(value="{{tag}}") {{name}} (RTL)
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
if rtl
|
||||
option(value="{{tag}}") {{name}} (RTL)
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
|
||||
template(name="defaultLayout")
|
||||
+header
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
.my-cards-board-wrapper {
|
||||
border-radius: 0 0 0.5vw 0.5vw;
|
||||
min-width: min(400px, 52vw);
|
||||
margin-bottom: 2.5vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-width: 0.3vw;
|
||||
border-style: solid;
|
||||
border-color: #a2a2a2;
|
||||
body.mobile-mode {
|
||||
.my-cards-board-wrapper {
|
||||
width: 100vw;
|
||||
}
|
||||
.my-cards-swimlane-body {
|
||||
grid-auto-flow: row;
|
||||
}
|
||||
}
|
||||
.my-cards-board-title {
|
||||
font-size: clamp(1.2rem, 3vw, 1.6rem);
|
||||
font-weight: bold;
|
||||
padding: 0.7vh 0.7vw;
|
||||
background-color: #808080;
|
||||
color: #fff;
|
||||
.my-cards-swimlane-body {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1ch;
|
||||
}
|
||||
.my-cards-swimlane-title {
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
font-size: clamp(1em, 2.5vw, 1.3rem);
|
||||
font-weight: bold;
|
||||
padding: 0.7vh 0.7vw;
|
||||
padding-bottom: 0.5vh;
|
||||
|
|
@ -27,48 +23,12 @@
|
|||
.swimlane-default-color {
|
||||
background-color: #d3d3d3;
|
||||
}
|
||||
.my-cards-list-title {
|
||||
font-weight: bold;
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
text-align: center;
|
||||
margin-bottom: 0.9vh;
|
||||
}
|
||||
.my-cards-list-wrapper {
|
||||
margin: 1.3vh 1.3vw;
|
||||
border-radius: 0.7vw;
|
||||
display: inline-grid;
|
||||
min-width: min(250px, 32vw);
|
||||
max-width: min(350px, 45vw);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: clamp(300px, 20vw, 30vw);
|
||||
}
|
||||
.my-cards-card-wrapper {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
.my-cards-dueat-list-wrapper {
|
||||
max-width: min(500px, 65vw);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.my-cards-board-table thead {
|
||||
border-bottom: 3px solid #4d4d4d;
|
||||
background-color: transparent;
|
||||
}
|
||||
.my-cards-board-table th,
|
||||
.my-cards-board-table td {
|
||||
border: 0;
|
||||
}
|
||||
.my-cards-board-table tr {
|
||||
border-bottom: 2px solid #a2a2a2;
|
||||
}
|
||||
.my-cards-card-title-table {
|
||||
font-weight: bold;
|
||||
padding-left: 2px;
|
||||
max-width: 243px;
|
||||
}
|
||||
.my-cards-board-badge {
|
||||
width: 36px;
|
||||
height: 24px;
|
||||
float: left;
|
||||
border-radius: 5px;
|
||||
margin-right: 5px;
|
||||
|
||||
body.mobile-mode .my-cards-list-wrapper {
|
||||
max-width: unset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,15 +39,16 @@ template(name="myCards")
|
|||
.my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}")
|
||||
+viewer
|
||||
= swimlane.title
|
||||
each list in swimlane.myLists
|
||||
.my-cards-list-wrapper
|
||||
.my-cards-list-title(class=list.colorClass)
|
||||
+viewer
|
||||
= list.title
|
||||
each card in list.myCards
|
||||
.my-cards-card-wrapper
|
||||
a.minicard-wrapper(href=card.originRelativeUrl)
|
||||
+minicard(card)
|
||||
.my-cards-swimlane-body
|
||||
each list in swimlane.myLists
|
||||
.my-cards-list-wrapper
|
||||
.my-cards-list-title(class=list.colorClass)
|
||||
+viewer
|
||||
= list.title
|
||||
each card in list.myCards
|
||||
.my-cards-card-wrapper
|
||||
a.minicard-wrapper(href=card.originRelativeUrl)
|
||||
+minicard(card)
|
||||
if $eq myCardsView 'table'
|
||||
.wrapper
|
||||
table.my-cards-board-table
|
||||
|
|
|
|||
|
|
@ -1,91 +1,121 @@
|
|||
.pop-over {
|
||||
background: #fff;
|
||||
border-radius: 0.4vw;
|
||||
border: 1px solid #dbdbdb;
|
||||
background: #ededed;
|
||||
border-bottom-color: #c2c2c2;
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
|
||||
position: absolute;
|
||||
/* Wider default to fit full color palette */
|
||||
width: min(380px, 55vw);
|
||||
z-index: 99999;
|
||||
margin-top: 0.7vh;
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0, 0, 0, 0.3);
|
||||
/* so they can easily travel with mouse */
|
||||
position: fixed;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
resize: both;
|
||||
pointer-events: all;
|
||||
max-height: 100vh;
|
||||
|
||||
.content-wrapper {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-wrapper >* {
|
||||
/* low specificity so that it can be transparently overriden,
|
||||
but could have side effects if no display is explicitely specific in inner content */
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pop-over a:has(.fa-plus)+ :not(*) {
|
||||
min-height: 1.5lh;
|
||||
aspect-ratio: 1/1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 0.2lh;
|
||||
}
|
||||
.pop-over hr {
|
||||
margin: 0.5vh 0px;
|
||||
margin: 0.3lh 0;
|
||||
/* below everything in the same stacking context when
|
||||
after, child or explicit z-index */
|
||||
z-index: 0;
|
||||
}
|
||||
.pop-over p,
|
||||
.pop-over textarea,
|
||||
.pop-over input[type="text"],
|
||||
.pop-over input[type="email"],
|
||||
.pop-over input[type="password"],
|
||||
.pop-over input[type="file"] {
|
||||
width: 100%;
|
||||
.pop-over {
|
||||
/* feels like it's too ad-hod */
|
||||
input, a:not(.js-board-template, .member, .edit-avatar) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
min-height: 1.5lh;
|
||||
}
|
||||
}
|
||||
.pop-over select {
|
||||
width: 100%;
|
||||
margin-bottom: 1.8vh;
|
||||
}
|
||||
.pop-over textarea {
|
||||
height: 9vh;
|
||||
}
|
||||
.pop-over form a span {
|
||||
padding: 0 0.7vw;
|
||||
.pop-over .sub-name {
|
||||
max-width: clamp(30vw, 500px, 80%);
|
||||
}
|
||||
.pop-over .header {
|
||||
height: 4.5vh;
|
||||
position: relative;
|
||||
margin-bottom: 1vh;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
padding: 0 1ch;
|
||||
background: #f7f7f7;
|
||||
border-bottom: 1px solid #dcdcdc;
|
||||
color: #666;
|
||||
min-height: 2lh;
|
||||
}
|
||||
.pop-over .header .header-title {
|
||||
display: block;
|
||||
line-height: 4vh;
|
||||
padding-top: 0.5vh;
|
||||
margin: 0 1.3vw;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.2em;
|
||||
flex: 1;
|
||||
cursor: grab !important;
|
||||
}
|
||||
.pop-over .header .back-btn {
|
||||
.pop-over .back-btn {
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
width: 4vw;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
.pop-over .header .back-btn i.fa {
|
||||
margin: 1.3vw;
|
||||
margin-top: 1.5vh;
|
||||
}
|
||||
.pop-over .header .back-btn.is-hidden {
|
||||
.pop-over .back-btn.is-hidden {
|
||||
width: 0;
|
||||
}
|
||||
.pop-over .header .close-btn {
|
||||
padding: 1.3vh 1.3vw 1.3vh 0.5vw;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.pop-over.no-title .header {
|
||||
background: none;
|
||||
}
|
||||
.pop-over .content-wrapper {
|
||||
width: 100%;
|
||||
max-height: calc(70vh + 20px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.pop-over {
|
||||
.content-wrapper, .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow dynamic max-height to override default constraint */
|
||||
.pop-over[style*="max-height"] .content-wrapper {
|
||||
max-height: inherit;
|
||||
.pop-over:has(.header) .content {
|
||||
/* inner content has full width available,
|
||||
so it is also responsive for margins, sizes, etc */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.popup-placeholder {
|
||||
/* This gives relative coordinates but height/width cannot fit the parent's
|
||||
without it having position: relative; we need to get them programmatically */
|
||||
position: absolute;
|
||||
/* Take all size of parent so it can be useful in computations */
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pop-over .content-container {
|
||||
width: 100%;
|
||||
max-height: calc(70vh + 20px);
|
||||
transition: transform 0.2s;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Allow dynamic max-height to override default constraint for content-container */
|
||||
|
|
@ -93,270 +123,42 @@
|
|||
max-height: inherit;
|
||||
}
|
||||
|
||||
/* Fix overflow in the Member Settings (member menu) popup:
|
||||
the popup itself gets a max-height inline style, but the header consumes space.
|
||||
Make the header overlay the scrollable area so the list can't spill out. */
|
||||
.pop-over[data-popup="memberMenuPopup"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
.pop-over[data-popup="memberMenuPopup"] > .header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.pop-over[data-popup="memberMenuPopup"] > .content-wrapper {
|
||||
padding-top: calc(4.5vh + 1vh);
|
||||
box-sizing: border-box;
|
||||
.pop-over .popup-drag-handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
/* Admin edit popups: use full height */
|
||||
.pop-over[data-popup="editUserPopup"],
|
||||
.pop-over[data-popup="editOrgPopup"],
|
||||
.pop-over[data-popup="editTeamPopup"] {
|
||||
height: calc(100vh - 20px) !important;
|
||||
max-height: calc(100vh - 20px) !important;
|
||||
body.mobile-mode {
|
||||
.popup-drag-handle, .close-btn {
|
||||
font-size: 1.4em;
|
||||
align-self: center;
|
||||
}
|
||||
.pop-over:has(.pop-over-list) {
|
||||
min-width: 70vw;
|
||||
}
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
overflow-y: auto !important;
|
||||
.pop-over .header-controls {
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-container,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-container,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-container {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
}
|
||||
|
||||
/* Ensure language popup list can scroll properly */
|
||||
.pop-over .pop-over-list {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Specific styling for language popup list */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Ensure content div in language popup contains all items */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
height: auto;
|
||||
/* Remove forced min-height to avoid top gap */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
font-size: 1.1rem;
|
||||
padding: 0 1ch;
|
||||
>li>a {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: fit-content;
|
||||
justify-content: start;
|
||||
padding: 0 0.5ch;
|
||||
column-gap: 1ch;
|
||||
.sub-name {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure hidden stack pages truly take no space */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
|
||||
min-height: 0 !important;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Make language popup extend to bottom of browser window */
|
||||
.pop-over[data-popup="changeLanguagePopup"] {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: 20px !important;
|
||||
width: auto !important;
|
||||
max-width: 450px !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
min-height: 300px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Allow dynamic height for Change Language popup */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .header {
|
||||
flex-shrink: 0 !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
|
||||
flex: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-container {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
padding-bottom: 50px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Date popup sizing for native HTML inputs */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"],
|
||||
.pop-over[data-popup="editCardStartDatePopup"],
|
||||
.pop-over[data-popup="editCardDueDatePopup"],
|
||||
.pop-over[data-popup="editCardEndDatePopup"],
|
||||
.pop-over[data-popup*="Date"] {
|
||||
width: min(400px, 90vw) !important; /* Smaller width for native inputs */
|
||||
min-width: 350px !important;
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup*="Date"] .content-wrapper {
|
||||
max-height: 60vh !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content-container,
|
||||
.pop-over[data-popup*="Date"] .content-container {
|
||||
max-height: 60vh !important;
|
||||
}
|
||||
|
||||
/* Native HTML input styling */
|
||||
.pop-over[data-popup*="Date"] .datepicker-container {
|
||||
width: 100% !important;
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields {
|
||||
display: flex !important;
|
||||
gap: 15px !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields .left,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields .right {
|
||||
flex: 1 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container label {
|
||||
display: block !important;
|
||||
margin-bottom: 5px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"],
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"] {
|
||||
width: 100% !important;
|
||||
padding: 8px !important;
|
||||
border: 1px solid #ccc !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 14px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"]:focus,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"]:focus {
|
||||
outline: none !important;
|
||||
border-color: #007cba !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Ensure date popup buttons stay within popup boundaries */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content,
|
||||
.pop-over[data-popup*="Date"] .content {
|
||||
max-height: 60vh !important; /* Leave space for buttons */
|
||||
overflow-y: auto !important;
|
||||
padding-bottom: 100px !important; /* More space for buttons */
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container {
|
||||
max-height: 50vh !important; /* Limit calendar height */
|
||||
overflow-y: auto !important;
|
||||
margin-bottom: 20px !important; /* Space before buttons */
|
||||
}
|
||||
|
||||
/* Ensure buttons are properly positioned */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date,
|
||||
.pop-over[data-popup*="Date"] .edit-date {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup*="Date"] .edit-date .fields {
|
||||
flex-shrink: 0 !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup*="Date"] .edit-date .js-datepicker {
|
||||
flex: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup*="Date"] .edit-date button {
|
||||
flex-shrink: 0 !important;
|
||||
margin-top: 15px !important;
|
||||
position: relative !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
.pop-over .content-container .content {
|
||||
/* Match wider popover, leave padding */
|
||||
width: 100%;
|
||||
padding: 0 1.3vw 1.3vh;
|
||||
box-sizing: border-box;
|
||||
/* Ensure content is not shifted left */
|
||||
margin-left: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Utility: remove left gutter inside specific popups */
|
||||
.pop-over .content .flush-left {
|
||||
margin-left: 0;
|
||||
|
|
@ -378,58 +180,15 @@
|
|||
.pop-over .content form.create-label .palette-colors {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
/* Color palette items: ensure proper positioning */
|
||||
.pop-over .content .palette-colors .palette-color {
|
||||
margin-left: 0;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Global fix for all popup content to prevent left shifting */
|
||||
.pop-over .content * {
|
||||
margin-left: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Override any potential left shifting for specific elements */
|
||||
.pop-over .content form,
|
||||
.pop-over .content .palette-colors,
|
||||
.pop-over .content .pop-over-list,
|
||||
.pop-over .content .flush-left {
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Fix popup depth containers that cause left shifting */
|
||||
.pop-over .popup-container-depth-1,
|
||||
.pop-over .popup-container-depth-2,
|
||||
.pop-over .popup-container-depth-3,
|
||||
.pop-over .popup-container-depth-4,
|
||||
.pop-over .popup-container-depth-5,
|
||||
.pop-over .popup-container-depth-6 {
|
||||
transform: none !important;
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
/* Ensure buttons don’t reserve left space; align to flow */
|
||||
.pop-over .content form.swimlane-color-popup .primary.confirm,
|
||||
.pop-over .content form.swimlane-color-popup .negate.wide.right,
|
||||
.pop-over .content .swimlane-height-popup .primary.confirm,
|
||||
.pop-over .content .swimlane-height-popup .negate.wide.right {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
.pop-over .content-container .content.no-height {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
border-radius: 0;
|
||||
outline: 0.1ch solid black;
|
||||
}
|
||||
.pop-over.search-over {
|
||||
background: #f0f0f0;
|
||||
|
|
@ -456,24 +215,6 @@
|
|||
.pop-over .sk-spinner {
|
||||
margin: 40px auto;
|
||||
}
|
||||
.pop-over .popup-container-depth-1 {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
.pop-over .popup-container-depth-2 {
|
||||
transform: translateX(-600px);
|
||||
}
|
||||
.pop-over .popup-container-depth-3 {
|
||||
transform: translateX(-900px);
|
||||
}
|
||||
.pop-over .popup-container-depth-4 {
|
||||
transform: translateX(-1200px);
|
||||
}
|
||||
.pop-over .popup-container-depth-5 {
|
||||
transform: translateX(-1500px);
|
||||
}
|
||||
.pop-over .popup-container-depth-6 {
|
||||
transform: translateX(-1800px);
|
||||
}
|
||||
.select-members-list,
|
||||
.select-avatars-list {
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -487,15 +228,12 @@
|
|||
cursor: pointer;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 1.5px 10px;
|
||||
padding-inline: 2vmin 10vmin;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
line-height: 33px;
|
||||
display:flex;
|
||||
/* flex-wrap:wrap;*/
|
||||
gap:5px;
|
||||
align-items: center;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
|
@ -506,7 +244,6 @@
|
|||
.pop-over-list li > a .item-name {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding-right: 22px;
|
||||
}
|
||||
.pop-over-list li > a:not(.disabled):hover {
|
||||
background-color: #005377;
|
||||
|
|
@ -522,9 +259,9 @@
|
|||
.pop-over-list li > a .sub-name {
|
||||
color: #8c8c8c;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
line-height: 15px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.pop-over-list li > a.current {
|
||||
background-color: #e2e6e9;
|
||||
|
|
@ -570,156 +307,21 @@
|
|||
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
.pop-over.miniprofile .header {
|
||||
border-bottom-color: transparent;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
z-index: 1;
|
||||
}
|
||||
.pop-over.miniprofile .header-title {
|
||||
display: none;
|
||||
}
|
||||
.pop-over.miniprofile .pop-over-list {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header {
|
||||
margin-top: 8px;
|
||||
min-height: 56px;
|
||||
position: relative;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .member,
|
||||
.pop-over.miniprofile .miniprofile-header .avatar {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .info {
|
||||
margin: 0 0 0 64px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .info h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .info h3 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.pop-over {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin-top: 0px;
|
||||
border: 0px solid #dbdbdb;
|
||||
/* Ensure popups appear above card details on mobile */
|
||||
z-index: 999999 !important;
|
||||
/* iOS Safari scrolling fix */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.pop-over .header {
|
||||
color: #fff;
|
||||
background: #2980b9;
|
||||
height: 48px;
|
||||
padding: 0px 0px;
|
||||
border: 0px;
|
||||
margin: 0px 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
.pop-over .header .header-title {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.pop-over .header .back-btn {
|
||||
width: 30px;
|
||||
padding: 8px 12px 8px 12px;
|
||||
}
|
||||
.pop-over .header .back-btn i.fa {
|
||||
color: #fff;
|
||||
}
|
||||
.pop-over .header .close-btn {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.pop-over .header .close-btn i.fa {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
.pop-over .content-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 48px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin: 48px 0px 0px 0px;
|
||||
}
|
||||
.pop-over .content-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.pop-over .content-container .content {
|
||||
width: calc(100% - 20px);
|
||||
height: calc(100% - 20px);
|
||||
padding: 10px;
|
||||
}
|
||||
.pop-over .content-container .content form {
|
||||
margin: 10px 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
.pop-over .content-container .content p,
|
||||
.pop-over .content-container .content textarea,
|
||||
.pop-over .content-container .content input[type="text"],
|
||||
.pop-over .content-container .content input[type="email"],
|
||||
.pop-over .content-container .content input[type="password"],
|
||||
.pop-over .content-container .content input[type="file"] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.pop-over .pop-over-list li > a {
|
||||
width: calc(100% - 20px);
|
||||
margin: 0px 0px;
|
||||
}
|
||||
.pop-over .popup-container-depth-1 {
|
||||
transform: none !important;
|
||||
}
|
||||
.pop-over .popup-container-depth-2 {
|
||||
transform: none !important;
|
||||
}
|
||||
.pop-over .popup-container-depth-3 {
|
||||
transform: none !important;
|
||||
}
|
||||
.pop-over .popup-container-depth-4 {
|
||||
transform: none !important;
|
||||
}
|
||||
.pop-over .popup-container-depth-5 {
|
||||
transform: none !important;
|
||||
}
|
||||
.pop-over .popup-container-depth-6 {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.pop-over .content > form {
|
||||
padding: 0 1ch;
|
||||
gap: 0.2lh;
|
||||
display: flex;
|
||||
max-width: clamp(20vw, 400px, 50vw);
|
||||
}
|
||||
|
||||
/* Force full-screen popups in mobile mode regardless of screen width */
|
||||
body.mobile-mode .pop-over {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
max-width: 100vw !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
width: 100% !important;
|
||||
height: calc(100vh - 48px) !important;
|
||||
max-height: calc(100vh - 48px) !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
body.mobile-mode .pop-over .content>form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pop-over .board-subtask-settings {
|
||||
>h3 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +1,696 @@
|
|||
Popup.template.events({
|
||||
'click .js-back-view'() {
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-close-pop-over'() {
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-confirm'() {
|
||||
this.__afterConfirmAction.call(this);
|
||||
},
|
||||
// This handler intends to solve a pretty tricky bug with our popup
|
||||
// transition. The transition is implemented using a large container
|
||||
// (.content-container) that is moved on the x-axis (from 0 to n*PopupSize)
|
||||
// inside a wrapper (.container-wrapper) with a hidden overflow. The problem
|
||||
// is that sometimes the wrapper is scrolled -- even if there are no
|
||||
// scrollbars. This happen for instance when the newly opened popup has some
|
||||
// focused field, the browser will automatically scroll the wrapper, resulting
|
||||
// in moving the whole popup container outside of the popup wrapper. To
|
||||
// disable this behavior we have to manually reset the scrollLeft position
|
||||
// whenever it is modified.
|
||||
'scroll .content-wrapper'(evt) {
|
||||
evt.currentTarget.scrollLeft = 0;
|
||||
},
|
||||
});
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
// When a popup content is removed (ie, when the user press the "back" button),
|
||||
// we need to wait for the container translation to end before removing the
|
||||
// actual DOM element. For that purpose we use the undocumented `_uihooks` API.
|
||||
Popup.template.onRendered(() => {
|
||||
const container = this.find('.content-container');
|
||||
container._uihooks = {
|
||||
removeElement(node) {
|
||||
$(node).addClass('no-height');
|
||||
$(container).one(CSSEvents.transitionend, () => {
|
||||
node.parentNode.removeChild(node);
|
||||
const PopupBias = {
|
||||
Before: Symbol("S"),
|
||||
Overlap: Symbol("M"),
|
||||
After: Symbol("A"),
|
||||
Fullscreen: Symbol("F"),
|
||||
includes(e) {
|
||||
return Object.values(this).includes(e);
|
||||
}
|
||||
}
|
||||
|
||||
// this class is a bit cumbersome and could probably be done simpler.
|
||||
// it manages two things : initial placement and sizing given an opener element,
|
||||
// and then movement and resizing. one difficulty was to be able, as a popup
|
||||
// which can be resized from the "outside" (CSS4) and move from the inside (inner
|
||||
// component), which also grows and shrinks frequently, to adapt.
|
||||
// I tried many approach and failed to get the perfect fit; I feel that there is
|
||||
// always something indeterminate at some point. so the only drawback is that
|
||||
// if a popup contains another resizable component (e.g. card details), and if
|
||||
// it has been resized (with CSS handle), it will lose its dimensions when dragging
|
||||
// it next time.
|
||||
class PopupDetachedComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
// Set by parent/caller (usually PopupComponent)
|
||||
({ nonPlaceholderOpener: this.nonPlaceholderOpener, closeDOMs: this.closeDOMs = [], followDOM: this.followDOM } = this.data());
|
||||
|
||||
|
||||
if (typeof(this.closeDOMs) === "string") {
|
||||
// helper for passing arg in JADE template
|
||||
this.closeDOMs = this.closeDOMs.split(';');
|
||||
}
|
||||
|
||||
// The popup's own header, if it exists
|
||||
this.closeDOMs.push("click .js-close-detached-popup");
|
||||
}
|
||||
|
||||
// Main intent of this component is to have a modular popup with defaults:
|
||||
// - sticks to its opener while being a child of body (thus in the same stacking context, no z-index issue)
|
||||
// - is responsive on shrink while keeping position absolute
|
||||
// - can grow back to initial position step by step
|
||||
// - exposes various sizes as CSS variables so each rendered popup can use them to adapt defaults
|
||||
// * issue is that it is done by hand, with heurisitic/simple algorithm from my thoughts, not sure it covers edge cases
|
||||
// * however it works well so far and maybe more "fixed" element should be popups
|
||||
onRendered() {
|
||||
// Remember initial ratio between initial dimensions and viewport
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
this.popup = this.firstNode();
|
||||
this.popupOpener = this.data().openerElement;
|
||||
|
||||
const popupStyle = window.getComputedStyle(this.firstNode());
|
||||
// margin may be in a relative unit, not computable in JS, but we get the actual pixels here
|
||||
this.popupMargin = parseFloat(popupStyle.getPropertyValue("--popup-margin"), 10) || Math.min(window.innerWidth / 50, window.innerHeight / 50);
|
||||
|
||||
this.dims(this.computeMaxDims());
|
||||
|
||||
this.initialPopupWidth = this.popupDims.width;
|
||||
this.initialPopupHeight = this.popupDims.height;
|
||||
this.initialHeightRatio = this.initialPopupHeight / viewportHeight;
|
||||
this.initialWidthRatio = this.initialPopupWidth / viewportWidth;
|
||||
|
||||
this.dims(this.computePopupDims());
|
||||
|
||||
|
||||
if (this.followDOM) {
|
||||
this.innerElement = this.find(this.followDOM) ?? document.querySelector(this.followDOM);
|
||||
}
|
||||
|
||||
this.follow();
|
||||
this.toFront();
|
||||
|
||||
// #FIXME the idea of keeping the initial ratio on resize is quite bad. remove that part.
|
||||
// there is a reactive variable for window resize in Utils, but the interface is too slow
|
||||
// with all reactive stuff, use events when possible and when not really bypassing logic
|
||||
$(window).on('resize', () => {
|
||||
// #FIXME there is a bug when window grows; popup outer container
|
||||
// will grow beyond the size of content and it's not easy to fix for me (and I feel tired of this popup)
|
||||
this.dims(this.computePopupDims());
|
||||
});
|
||||
}
|
||||
|
||||
margin() {
|
||||
return this.popupMargin;
|
||||
}
|
||||
|
||||
ensureDimsLimit(dims) {
|
||||
// boilerplate to make sure that popup visually fits
|
||||
let { left, top, width, height } = dims;
|
||||
let overflowBottom = top + height + 2 * this.margin() - window.innerHeight;
|
||||
let overflowRight = left + width + 2 * this.margin() - window.innerWidth;
|
||||
if (overflowRight > 0) {
|
||||
width = Math.max(20 * this.margin(), Math.min(width - overflowRight, window.innerWidth - 2 * this.margin()));
|
||||
}
|
||||
if (overflowBottom > 0) {
|
||||
height = Math.max(10 * this.margin(), Math.min(height - overflowBottom, window.innerHeight - 2 * this.margin()));
|
||||
}
|
||||
left = Math.max(left, this.margin());
|
||||
top = Math.max(top, this.margin());
|
||||
return { left, top, width, height }
|
||||
}
|
||||
|
||||
dims(newDims) {
|
||||
if (!this.popupDims) {
|
||||
this.popupDims = {};
|
||||
}
|
||||
if (newDims) {
|
||||
newDims = this.ensureDimsLimit(newDims);
|
||||
for (const e of Object.keys(newDims)) {
|
||||
let value = parseFloat(newDims[e]);
|
||||
if (!isNaN(value)) {
|
||||
$(this.popup).css(e, `${value}px`);
|
||||
this.popupDims[e] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.popupDims;
|
||||
}
|
||||
|
||||
isFullscreen() {
|
||||
return this.fullscreen;
|
||||
}
|
||||
|
||||
maximize() {
|
||||
this.fullscreen = true;
|
||||
this.dims(this.computePopupDims());
|
||||
if (this.innerElement) {
|
||||
$(this.innerElement).css('width', '');
|
||||
$(this.innerElement).css('height', '')
|
||||
}
|
||||
}
|
||||
|
||||
minimize() {
|
||||
this.fullscreen = false;
|
||||
this.dims(this.computePopupDims());
|
||||
}
|
||||
|
||||
follow() {
|
||||
const adaptChild = new ResizeObserver((_) => {
|
||||
if (this.fullscreen) {return}
|
||||
const width = this.innerElement?.scrollWidth || this.popup.scrollWidth;
|
||||
const height = this.innerElement?.scrollHeight || this.popup.scrollHeight;
|
||||
// we don't want to run this during something that we have caused, eg. dragging
|
||||
if (!this.mouseDown) {
|
||||
// extra-"future-proof" stuff: if somebody adds a margin to the popup, it would trigger a loop
|
||||
if (Math.abs(this.dims().width - width) < 20 && Math.abs(this.dims().height - height) < 20) { return }
|
||||
|
||||
// if inner shrinks, follow
|
||||
if (width < this.dims().width || height < this.dims().height) {
|
||||
this.dims({ width, height });
|
||||
}
|
||||
// otherwise it may be complicated to find a generic situation, but we have the
|
||||
// classic positionning procedure which works, so use it and ignore positionning
|
||||
else {
|
||||
const newDims = this.computePopupDims();
|
||||
// a bit twisted/ad-hoc for card details, in the edge case where they are opened when collapsed then uncollapsed,
|
||||
// not sure to understand why the sizing works differently that starting uncollapsed then doing the same sequence
|
||||
this.dims(this.ensureDimsLimit({
|
||||
top: this.dims().top,
|
||||
left: this.dims().left,
|
||||
width: Math.max(newDims.width, width),
|
||||
height: Math.max(newDims.height, height)
|
||||
}));
|
||||
}
|
||||
}
|
||||
else {
|
||||
const { width, height } = this.popup.getBoundingClientRect();
|
||||
// only case when we bypass .dims(), to avoid loop
|
||||
this.popupDims.width = width;
|
||||
this.popupDims.height = height;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.innerElement) {
|
||||
adaptChild.observe(this.innerElement);
|
||||
} else {
|
||||
adaptChild.observe(this.popup);
|
||||
}
|
||||
}
|
||||
|
||||
currentZ(z = undefined) {
|
||||
// relative, add a constant to be above root elements
|
||||
if (z !== undefined) {
|
||||
this.firstNode().style.zIndex = parseInt(z) + 10;
|
||||
}
|
||||
return parseInt(this.firstNode().style.zIndex) - 10;
|
||||
}
|
||||
|
||||
// a bit complex...
|
||||
toFront() {
|
||||
this.currentZ(Math.max(...PopupComponent.stack.map(p => BlazeComponent.getComponentForElement(p.outerView.firstNode()).currentZ())) || 0 + 1);
|
||||
|
||||
}
|
||||
|
||||
toBack() {
|
||||
this.currentZ(Math.min(...PopupComponent.stack.map(p => BlazeComponent.getComponentForElement(p.outerView.firstNode()).currentZ())) || 1 - 1);
|
||||
}
|
||||
|
||||
events() {
|
||||
// needs to be done at this level; "parent" is not a parent in DOM
|
||||
let closeEvents = {};
|
||||
|
||||
this.closeDOMs?.forEach((e) => {
|
||||
closeEvents[e] = (_) => {
|
||||
this.parentComponent().destroy();
|
||||
}
|
||||
})
|
||||
|
||||
const miscEvents = {
|
||||
'click .js-confirm'() {
|
||||
this.data().afterConfirm?.call(this);
|
||||
},
|
||||
// bad heuristic but only for best-effort UI
|
||||
'pointerdown .pop-over'() {
|
||||
this.mouseDown = true;
|
||||
},
|
||||
'pointerup .pop-over'() {
|
||||
this.mouseDown = false;
|
||||
}
|
||||
};
|
||||
|
||||
const movePopup = (event) => {
|
||||
event.preventDefault();
|
||||
$(event.target).addClass('is-active');
|
||||
const deltaHandleX = this.dims().left - event.clientX;
|
||||
const deltaHandleY = this.dims().top - event.clientY;
|
||||
|
||||
const onPointerMove = (e) => {
|
||||
this.dims(this.ensureDimsLimit({ left: e.clientX + deltaHandleX, top: e.clientY + deltaHandleY, width: this.dims().width, height: this.dims().height }));
|
||||
|
||||
if (this.popup.scrollY) {
|
||||
this.popup.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = (event) => {
|
||||
$(document).off('pointermove', onPointerMove);
|
||||
$(document).off('pointerup', onPointerUp);
|
||||
$(event.target).removeClass('is-active');
|
||||
};
|
||||
|
||||
if (Utils.shouldIgnorePointer(event)) {
|
||||
onPointerUp(event);
|
||||
return;
|
||||
}
|
||||
|
||||
$(document).on('pointermove', onPointerMove);
|
||||
$(document).on('pointerup', onPointerUp);
|
||||
};
|
||||
|
||||
// We do not manage dragging without our own header
|
||||
const handleDOM = this.data().handleDOM;
|
||||
if (this.data().showHeader) {
|
||||
const handleSelector = Utils.isMiniScreen() ? '.js-popup-drag-handle' : '.header-title';
|
||||
miscEvents[`pointerdown ${handleSelector}`] = (e) => movePopup(e);
|
||||
}
|
||||
if (handleDOM) {
|
||||
miscEvents[`pointerdown ${handleDOM}`] = (e) => movePopup(e);
|
||||
}
|
||||
return super.events().concat(closeEvents).concat(miscEvents);
|
||||
}
|
||||
|
||||
computeMaxDims() {
|
||||
// Get size of inner content, even if it overflows
|
||||
const content = this.find('.content');
|
||||
let popupHeight = content.scrollHeight;
|
||||
let popupWidth = content.scrollWidth;
|
||||
if (this.data().showHeader) {
|
||||
const headerRect = this.find('.header');
|
||||
popupHeight += headerRect.scrollHeight;
|
||||
popupWidth = Math.max(popupWidth, headerRect.scrollWidth)
|
||||
}
|
||||
return { width: Math.max(popupWidth, $(this.popup).width()), height: Math.max(popupHeight, $(this.popup).height()) };
|
||||
|
||||
}
|
||||
|
||||
placeOnSingleDimension(elementLength, openerPos, openerLength, maxLength, biases, n) {
|
||||
// avoid too much recursion if no solution
|
||||
if (!n) {
|
||||
n = 0;
|
||||
}
|
||||
if (n >= 5) {
|
||||
// if we exhausted a bias, remove it
|
||||
n = 0;
|
||||
biases.pop();
|
||||
if (biases.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
n += 1;
|
||||
}
|
||||
|
||||
if (!biases?.length) {
|
||||
const cut = maxLength / 3;
|
||||
|
||||
if (openerPos < cut) {
|
||||
// Corresponds to the default ordering: if element is close to the axe's start,
|
||||
// try to put the popup after it; then to overlap; and give up otherwise.
|
||||
biases = [PopupBias.After, PopupBias.Overlap]
|
||||
}
|
||||
else if (openerPos > 2 * cut) {
|
||||
// Same idea if popup is close to the end
|
||||
biases = [PopupBias.Before, PopupBias.Overlap]
|
||||
}
|
||||
else {
|
||||
// If in the middle, try to overlap: choosing between start or end, even for
|
||||
// default, is too arbitrary; a custom order can be passed in argument.
|
||||
biases = [PopupBias.Overlap]
|
||||
}
|
||||
}
|
||||
// Remove the first element and get it
|
||||
const bias = biases.splice(0, 1)[0];
|
||||
|
||||
let factor;
|
||||
const openerRef = openerPos + openerLength / 2;
|
||||
if (bias === PopupBias.Before) {
|
||||
factor = 1;
|
||||
}
|
||||
else if (bias === PopupBias.Overlap) {
|
||||
factor = openerRef / maxLength;
|
||||
}
|
||||
else {
|
||||
factor = 0;
|
||||
}
|
||||
|
||||
let candidatePos = openerRef - elementLength * factor;
|
||||
const deltaMax = candidatePos + elementLength - maxLength;
|
||||
if (candidatePos < 0 || deltaMax > 0) {
|
||||
if (deltaMax <= 2 * this.margin()) {
|
||||
// if this is just a matter of margin, try again
|
||||
// useful for (literal) corner cases
|
||||
biases = [bias].concat(biases);
|
||||
openerPos -= 5;
|
||||
}
|
||||
if (biases.length === 0) {
|
||||
// we could have returned candidate position even if the size is too large, so
|
||||
// that the caller can choose, but it means more computations and edge cases...
|
||||
// any negative means fullscreen overall as the caller will take the maximum between
|
||||
// margin and candidate.
|
||||
return -1;
|
||||
}
|
||||
return this.placeOnSingleDimension(elementLength, openerPos, openerLength, maxLength, biases, n);
|
||||
}
|
||||
return candidatePos;
|
||||
}
|
||||
|
||||
computePopupDims() {
|
||||
if (!this.isRendered?.()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Coordinates of opener related to viewport
|
||||
let { x: parentX, y: parentY } = this.nonPlaceholderOpener.getBoundingClientRect();
|
||||
let { height: parentHeight, width: parentWidth } = this.nonPlaceholderOpener.getBoundingClientRect();
|
||||
|
||||
// Initial dimensions scaled to the viewport, if it has changed
|
||||
let popupHeight = window.innerHeight * this.initialHeightRatio;
|
||||
let popupWidth = window.innerWidth * this.initialWidthRatio;
|
||||
|
||||
if (this.fullscreen || Utils.isMiniScreen() && popupWidth >= 4 * window.innerWidth / 5 && popupHeight >= 4 * window.innerHeight / 5) {
|
||||
// Go fullscreen!
|
||||
popupWidth = window.innerWidth;
|
||||
// Avoid address bar, let a bit of margin to scroll
|
||||
popupHeight = 4 * window.innerHeight / 5;
|
||||
return ({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Current viewport dimensions
|
||||
let maxHeight = window.innerHeight - this.margin() * 2;
|
||||
let maxWidth = window.innerWidth - this.margin() * 2;
|
||||
let biasX, biasY;
|
||||
if (Utils.isMiniScreen()) {
|
||||
// On mobile I found that being able to close a popup really close from where it has been clicked
|
||||
// is comfortable; so given that the close button is top-right, we prefer the position of
|
||||
// popup being right-bottom, when possible. We then try every position, rather than choosing
|
||||
// relatively to the relative position of opener in viewport
|
||||
biasX = [PopupBias.Before, PopupBias.Overlap, PopupBias.After];
|
||||
biasY = [PopupBias.After, PopupBias.Overlap, PopupBias.Before];
|
||||
}
|
||||
|
||||
const candidateX = this.placeOnSingleDimension(popupWidth, parentX, parentWidth, maxWidth, biasX);
|
||||
const candidateY = this.placeOnSingleDimension(popupHeight, parentY, parentHeight, maxHeight, biasY);
|
||||
|
||||
// Reasonable defaults that can be overriden by CSS later: popups are tall, try to fit the reste
|
||||
// of the screen starting from parent element, or full screen if element if not fitting
|
||||
return ({
|
||||
width: popupWidth,
|
||||
height: popupHeight,
|
||||
left: candidateX,
|
||||
top: candidateY,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PopupComponent extends BlazeComponent {
|
||||
static stack = [];
|
||||
// good enough as long as few occurences of such cases
|
||||
static multipleBlacklist = ["cardDetails"];
|
||||
|
||||
// to provide compatibility with Popup.open().
|
||||
static open(args) {
|
||||
const openerView = Blaze.getView(args.openerElement);
|
||||
if (!openerView) {
|
||||
console.warn(`no parent found for popup ${args.name}, attaching to body: this should not happen`);
|
||||
}
|
||||
|
||||
|
||||
// render ourselves; everything is automatically managed from that moment, we just added
|
||||
// a level of indirection but this will not interfere with data
|
||||
const popup = new PopupComponent();
|
||||
Blaze.renderWithData(
|
||||
popup.renderComponent(BlazeComponent.currentComponent()),
|
||||
args,
|
||||
args.openerElement,
|
||||
null,
|
||||
openerView
|
||||
);
|
||||
return popup;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
PopupComponent.stack.at(-1)?.destroy();
|
||||
}
|
||||
|
||||
static findParentPopup(element) {
|
||||
return BlazeComponent.getComponentForElement($(element).closest('.pop-over')[0]);
|
||||
}
|
||||
|
||||
static toFront(event) {
|
||||
const popup = PopupComponent.findParentPopup(event.target)
|
||||
popup?.toFront();
|
||||
return popup;
|
||||
}
|
||||
|
||||
static toBack(event) {
|
||||
const popup = PopupComponent.findParentPopup(event.target);
|
||||
popup?.toBack();
|
||||
return popup;
|
||||
}
|
||||
|
||||
static maximize(event) {
|
||||
const popup = PopupComponent.findParentPopup(event.target);
|
||||
popup?.toFront();
|
||||
popup?.maximize();
|
||||
return popup;
|
||||
}
|
||||
|
||||
static minimize(event) {
|
||||
const popup = PopupComponent.findParentPopup(event.target);
|
||||
popup?.minimize();
|
||||
return popup;
|
||||
}
|
||||
|
||||
|
||||
getOpenerElement(view) {
|
||||
// Look for the first parent view whose first DOM element is not virtually us
|
||||
const firstNode = $(view.firstNode());
|
||||
|
||||
// The goal is to have the best chances to get the element whose size and pos
|
||||
// are relevant; e.g. when clicking on a date on a minicard, we don't wan't
|
||||
// the opener to be set to the minicard.
|
||||
// In order to work in general, we need to take special situations into account,
|
||||
// e.g. the placeholder is isolated, or does not have previous node, and so on.
|
||||
// In general we prefer previous node, then next, then any displayed sibling,
|
||||
// then the parent, and so on.
|
||||
let candidates = [];
|
||||
if (!firstNode.hasClass(this.popupPlaceholderClass())) {
|
||||
candidates.push(firstNode);
|
||||
}
|
||||
candidates = candidates.concat([firstNode.prev(), firstNode.next()]);
|
||||
const otherSiblings = Array.from(firstNode.siblings()).filter(e => !candidates.includes(e));
|
||||
|
||||
for (const cand of candidates.concat(otherSiblings)) {
|
||||
const displayCSS = cand?.css("display");
|
||||
if (displayCSS && displayCSS !== "none") {
|
||||
return cand[0];
|
||||
}
|
||||
}
|
||||
return this.getOpenerElement(view.parentView);
|
||||
}
|
||||
|
||||
getParentData(view) {;
|
||||
let data;
|
||||
// ⚠️ node can be a text node
|
||||
while (view.firstNode?.()?.classList?.contains(this.popupPlaceholderClass())) {
|
||||
view = view.parentView;
|
||||
data = Blaze.getData(view);
|
||||
}
|
||||
// This is VERY IMPORTANT to get data like this and not with templateInstance.data,
|
||||
// because this form is reactive. So all inner popups have reactive data, which is nice
|
||||
return data;
|
||||
}
|
||||
|
||||
onCreated() {
|
||||
// #FIXME prevent secondary popups to open
|
||||
// Special "magic number" case: never render, for any reason, the same card
|
||||
// const maybeID = this.parentComponent?.()?.data?.()?._id;
|
||||
// if (maybeID && PopupComponent.stack.find(e => e.parentComponent().data?.()?._id === maybeID)) {
|
||||
// this.destroy();
|
||||
// return;
|
||||
// }
|
||||
// do not render a template multiple times
|
||||
const existing = PopupComponent.stack.find((e) => (e.name == this.data().name));
|
||||
if (existing && PopupComponent.multipleBlacklist.indexOf(this.data().name)) {
|
||||
// ⚠️ is there a default better than another? I feel that closing existing
|
||||
// popup is not bad in general because having the same button for open and close
|
||||
// is common
|
||||
if (PopupComponent.multipleBlacklist.includes(existing.name)) {
|
||||
existing.destroy();
|
||||
}
|
||||
// but is could also be re-rendering, eg
|
||||
// existing.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// All of this, except name, is optional. The rest is provided "just in case", for convenience (hopefully)
|
||||
//
|
||||
// - name is the name of a template to render inside the popup (to the detriment of its size) or the contrary
|
||||
// - showHeader can be turned off if the inner content always have a header with buttons and so on
|
||||
// - title is shown when header is shown
|
||||
// - miscOptions is for compatibility
|
||||
// - closeVar is an optional string representing a Session variable: if set, the popup reactively closes when the variable changes and set the variable to null on close
|
||||
// - closeDOMs can be used alternatively; it is an array of "<event> <selector>" to listen that closes the popup.
|
||||
// if header is shown, closing the popup is already managed. selector is relative to the inner template (same as its event map)
|
||||
// - followDOM is an element whose dimension will serve as reference so that popup can react to inner changes; works only with inline styles (otherwise we probably would need IntersectionObserver-like stuff, async etc)
|
||||
// - handleDOM is an element who can be clicked to move popup
|
||||
// it is useful when the content can be redimensionned/moved by code or user; we still manage events, resizes etc
|
||||
// but allow inner elements or handles to do it (and we adapt).
|
||||
const data = this.data();
|
||||
this.popupArgs = {
|
||||
name: data.name,
|
||||
showHeader: data.showHeader ?? true,
|
||||
title: data.title,
|
||||
openerElement: data.openerElement,
|
||||
closeDOMs: data.closeDOMs,
|
||||
followDOM: data.followDOM,
|
||||
handleDOM: data.handleDOM,
|
||||
forceData: data.miscOptions?.dataContextIfCurrentDataIsUndefined,
|
||||
afterConfirm: data.miscOptions?.afterConfirm,
|
||||
}
|
||||
this.name = this.data().name;
|
||||
|
||||
this.innerTemplate = Template[this.name];
|
||||
this.innerComponent = BlazeComponent.getComponent(this.name);
|
||||
|
||||
this.outerComponent = BlazeComponent.getComponent('popupDetached');
|
||||
if (!(this.innerComponent || this.innerTemplate)) {
|
||||
throw new Error(`template and/or component ${this.name} not found`);
|
||||
}
|
||||
|
||||
// If arg is not set, must be closed manually by calling destroy()
|
||||
if (this.popupArgs.closeVar) {
|
||||
this.closeInitialValue = Session.get(this.data().closeVar);
|
||||
if (!this.closeInitialValue === undefined) {
|
||||
this.autorun(() => {
|
||||
if (Session.get(this.data().closeVar) !== this.closeInitialValue) {
|
||||
this.onDestroyed();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popupPlaceholderClass() {
|
||||
return "popup-placeholder";
|
||||
}
|
||||
|
||||
render() {
|
||||
const oldOuterView = this.outerView;
|
||||
// see below for comments
|
||||
this.outerView = Blaze.renderWithData(
|
||||
// data is passed through the parent relationship
|
||||
// we need to render it again to keep events in sync with inner popup
|
||||
this.outerComponent.renderComponent(this.component()),
|
||||
this.popupArgs,
|
||||
document.body,
|
||||
null,
|
||||
this.openerView
|
||||
);
|
||||
this.innerView = Blaze.renderWithData(
|
||||
// the template to render: either the content is a BlazeComponent or a regular template
|
||||
// if a BlazeComponent, render it as a template first
|
||||
this.innerComponent?.renderComponent?.(this.component()) || this.innerTemplate,
|
||||
// dataContext used for rendering: each time we go find data, because it is non-reactive
|
||||
() => (this.popupArgs.forceData || this.getParentData(this.currentView)),
|
||||
// DOM parent: ask to the detached popup, will be inserted at the last child
|
||||
this.outerView.firstNode()?.getElementsByClassName('content')?.[0] || document.body,
|
||||
// "stop" DOM element; we don't use
|
||||
null,
|
||||
// important: this is the Blaze.View object which will be set as `parentView` of
|
||||
// the rendered view. we set it as the parent view, so that the detached popup
|
||||
// can interact with its "parent" without being a child of it, and without
|
||||
// manipulating DOM directly.
|
||||
this.openerView
|
||||
);
|
||||
if (oldOuterView) {
|
||||
Blaze.remove(oldOuterView);
|
||||
}
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
if (this.detached) {return}
|
||||
// Use plain Blaze stuff to be able to render all templates, but use components when available/relevant
|
||||
this.currentView = Blaze.currentView || Blaze.getView(this.component().firstNode());
|
||||
|
||||
// Placement will be related to the opener (usually clicked element)
|
||||
// But template data and view related to the opener are not the same:
|
||||
// - view is probably outer, as is was already rendered on click
|
||||
// - template data could be found with Template.parentData(n), but `n` can
|
||||
// vary depending on context: using those methods feels more reliable for this use case
|
||||
this.popupArgs.openerElement ??= this.getOpenerElement(this.currentView);
|
||||
this.openerView = Blaze.getView(this.popupArgs.openerElement);
|
||||
// With programmatic/click opening, we get the "real" opener; with dynamic
|
||||
// templating we get the placeholder and need to go up to get a glimpse of
|
||||
// the "real" opener size. It is quite imprecise in that case (maybe the
|
||||
// interesting opener is a sibling, not an ancestor), but seems to do the job
|
||||
// for now.
|
||||
// Also it feels sane that inner content does not have a reference to
|
||||
// a virtual placeholder.
|
||||
const opener = this.popupArgs.openerElement;
|
||||
let sizedOpener = opener;
|
||||
if (opener.classList?.contains?.(this.popupPlaceholderClass())) {
|
||||
sizedOpener = opener.parentNode;
|
||||
}
|
||||
this.popupArgs.nonPlaceholderOpener = sizedOpener;
|
||||
|
||||
PopupComponent.stack.push(this);
|
||||
|
||||
try {
|
||||
this.render();
|
||||
// Render above other popups by default
|
||||
} catch(e) {
|
||||
// If something went wrong during rendering, do not create
|
||||
// "zombie" popups
|
||||
console.error(`cannot render popup ${this.name}: ${e}`);
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.detached = true;
|
||||
if (!PopupComponent.stack.includes(this)) {
|
||||
// Avoid loop destroy
|
||||
return;
|
||||
}
|
||||
// Maybe overkill but may help to avoid leaking memory
|
||||
// as programmatic rendering is less usual
|
||||
for (const view of [this.innerView, this.currentView, this.outerView]) {
|
||||
try {
|
||||
Blaze.remove(view);
|
||||
} catch {
|
||||
console.warn(`A view failed to be removed: ${view}`)
|
||||
}
|
||||
}
|
||||
this.innerComponent?.removeComponent?.();
|
||||
this.outerComponent?.removeComponent?.();
|
||||
this.removeComponent();
|
||||
|
||||
// not necesserly removed in order, e.g. multiple cards
|
||||
PopupComponent.stack = PopupComponent.stack.filter(e => e !== this);
|
||||
}
|
||||
|
||||
|
||||
closeWithPlaceholder(parentElement) {
|
||||
// adapted from https://stackoverflow.com/questions/52834774/dom-event-when-element-is-removed
|
||||
// strangely, when opener is removed because of a reactive change, this component
|
||||
// do not get any lifecycle hook called, so we need to bridge the gap. Simply
|
||||
// "close" popup when placeholder is off-DOM.
|
||||
while (parentElement.nodeType === Node.TEXT_NODE) {
|
||||
parentElement = parentElement.parentElement;
|
||||
}
|
||||
const placeholder = parentElement.getElementsByClassName(this.popupPlaceholderClass());
|
||||
if (!placeholder.length) {
|
||||
return;
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
// DOM element being suppressed is reflected in array
|
||||
if (placeholder.length === 0) {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
observer.observe(parentElement, {childList: true});
|
||||
}
|
||||
}
|
||||
|
||||
PopupComponent.register("popup");
|
||||
PopupDetachedComponent.register('popupDetached');
|
||||
|
||||
export default PopupComponent;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
.pop-over.js-pop-over(
|
||||
class="{{#unless title}}miniprofile{{/unless}}"
|
||||
class=currentBoard.colorClass
|
||||
class="{{#unless title}}no-title{{/unless}}"
|
||||
data-popup="{{popupName}}"
|
||||
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
|
||||
.header
|
||||
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
|
||||
i.fa.fa-caret-left
|
||||
span.header-title= title
|
||||
a.close-btn.js-close-pop-over
|
||||
i.fa.fa-times-thin
|
||||
.content-wrapper
|
||||
//-
|
||||
We display the all stack of popup content next to each other and move
|
||||
the "window" by translating .content-container inside .content-wrapper.
|
||||
.content-container(class="popup-container-depth-{{depth}}")
|
||||
each stack
|
||||
//-
|
||||
XXX We need a better way to express the "is the last element" condition.
|
||||
Hopefully the @last helper will come soon (or at least @index)
|
||||
.content(class="{{#unless $eq popupName ../popupName}}no-height{{/unless}}")
|
||||
+Template.dynamic(template=popupName data=dataContext)
|
||||
.clearfix
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
height: 50px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
|
||||
}
|
||||
.sk-spinner-wave div {
|
||||
background-color: #333;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue