mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
refactor(schedule): consolidate scrollbars onto single parent element
- Move horizontal scroll control to parent .scroll-wrapper element - Both vertical and horizontal scrollbars now on same container - Pass isHorizontalScrollMode as input to schedule-week component - Remove duplicate scroll wrapper from schedule-week - Maintain responsive column widths based on scroll mode - Fixes scrollbar positioning and coordination issues This ensures both scrollbars are managed by the same element, providing better UX and preventing scrollbar positioning conflicts.
This commit is contained in:
parent
f4d3c61ec9
commit
d2ab8e6482
6 changed files with 227 additions and 230 deletions
|
|
@ -1,191 +1,189 @@
|
|||
<div class="horizontal-scroll-wrapper">
|
||||
<header
|
||||
class="week-header"
|
||||
[class.isInPanel]="isInPanel()"
|
||||
>
|
||||
<div class="days">
|
||||
<div class="filler"><!--for time --></div>
|
||||
@for (day of daysToShow(); track $index) {
|
||||
<div class="day">
|
||||
@if (day === todayDateStr()) {
|
||||
<mat-icon>wb_sunny</mat-icon>
|
||||
}
|
||||
<div class="day-num">{{ day | localeDate: 'd' }}</div>
|
||||
<div class="day-day">{{ day | localeDate: 'EEE' }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
#gridContainer
|
||||
class="grid-container"
|
||||
(click)="onGridClick($event)"
|
||||
(mousemove)="onMoveOverGrid($event)"
|
||||
>
|
||||
<!-- Time -->
|
||||
@for (time of times(); track $index) {
|
||||
<div
|
||||
class="time"
|
||||
style="grid-row: {{ $index * FH + 1 }}"
|
||||
>
|
||||
{{ time }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Grid Rows -->
|
||||
@for (row of rowsByNr; track $index) {
|
||||
<div
|
||||
class="row"
|
||||
style="grid-row: {{ row + 1 }}"
|
||||
></div>
|
||||
}
|
||||
|
||||
<!-- Grid Cols -->
|
||||
<header
|
||||
class="week-header"
|
||||
[class.isInPanel]="isInPanel()"
|
||||
>
|
||||
<div class="days">
|
||||
<div class="filler"><!--for time --></div>
|
||||
@for (day of daysToShow(); track $index) {
|
||||
<div
|
||||
class="col"
|
||||
[attr.data-day]="day"
|
||||
style="grid-column: {{ $index + 2 }}; grid-row: 1 / span {{
|
||||
($index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart()) - 1
|
||||
}}"
|
||||
></div>
|
||||
<div
|
||||
class="col end-of-day"
|
||||
[attr.data-day]="day"
|
||||
style="grid-column: {{ $index + 2 }}; grid-row: {{
|
||||
$index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart()
|
||||
}} / span {{
|
||||
totalRows -
|
||||
($index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart())
|
||||
}}"
|
||||
></div>
|
||||
}
|
||||
|
||||
<!-- Work Start and End -->
|
||||
@if (workStartEnd()) {
|
||||
<div
|
||||
id="work-start"
|
||||
class="work-start"
|
||||
style="grid-row: {{ workStartEnd()!.workStartRow }}"
|
||||
>
|
||||
<div>{{ T.F.SCHEDULE.START | translate }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="work-end"
|
||||
style="grid-row: {{ workStartEnd()!.workEndRow }}"
|
||||
>
|
||||
<div>{{ T.F.SCHEDULE.END | translate }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (currentTimeRow() !== null) {
|
||||
<div
|
||||
id="current-time"
|
||||
class="current-time"
|
||||
style="grid-column: 2; grid-row: {{ currentTimeRow() }} / span 1"
|
||||
>
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Events -->
|
||||
@for (ev of safeEvents(); track ev.id) {
|
||||
@if (isDraggableSE(ev)) {
|
||||
<schedule-event
|
||||
class="draggable"
|
||||
[cdkDragData]="ev"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
(cdkDragStarted)="dragStarted($event)"
|
||||
(cdkDragReleased)="dragReleased($event)"
|
||||
[cdkDragStartDelay]="IS_TOUCH_PRIMARY ? 75 : 0"
|
||||
[event]="ev"
|
||||
></schedule-event>
|
||||
} @else {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
[cdkDragDisabled]="true"
|
||||
></schedule-event>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Excess tasks planned for day -->
|
||||
@for (beyondBudgetDay of safeBeyondBudget(); track i; let i = $index) {
|
||||
@if (beyondBudgetDay.length > 0) {
|
||||
<div
|
||||
class="excess-entries"
|
||||
style="grid-column: {{ i + 2 }}"
|
||||
>
|
||||
<div
|
||||
class="excess-entries-header"
|
||||
[matTooltipPosition]="'above'"
|
||||
[matTooltip]="
|
||||
'Tasks planned for day, but that are beyond the available time budget'
|
||||
"
|
||||
>
|
||||
<mat-icon>hourglass_disabled</mat-icon>
|
||||
{{ beyondBudgetDay.length }}
|
||||
</div>
|
||||
@for (ev of beyondBudgetDay; track ev.id) {
|
||||
@if (isDraggableSE(ev)) {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
class="draggable"
|
||||
[cdkDragData]="ev"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
(cdkDragStarted)="dragStarted($event)"
|
||||
(cdkDragReleased)="dragReleased($event)"
|
||||
[cdkDragStartDelay]="IS_TOUCH_PRIMARY ? DRAG_DELAY_FOR_TOUCH : 0"
|
||||
></schedule-event>
|
||||
} @else {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
[cdkDragDisabled]="true"
|
||||
></schedule-event>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (
|
||||
(!isTaskDragActive() || isCtrlPressed()) && newTaskPlaceholder();
|
||||
as newTaskPlaceholder
|
||||
) {
|
||||
<create-task-placeholder
|
||||
[isEditMode]="isCreateTaskActive()"
|
||||
[time]="newTaskPlaceholder.time"
|
||||
[date]="newTaskPlaceholder.date"
|
||||
(editEnd)="isCreateTaskActive.set(false); this.newTaskPlaceholder.set(null)"
|
||||
[style]="newTaskPlaceholder.style"
|
||||
[style.opacity]="isCtrlPressed() ? 1 : null"
|
||||
>
|
||||
</create-task-placeholder>
|
||||
}
|
||||
|
||||
@if (isDragging() && currentDragEvent() && dragPreviewStyle()) {
|
||||
<schedule-event
|
||||
class="custom-drag-preview"
|
||||
[event]="currentDragEvent()!"
|
||||
[style]="dragPreviewStyle()!"
|
||||
[class.isShiftInsertPreview]="dragPreviewContext()?.kind === 'shift-task'"
|
||||
[class.isScheduleForDay]="isShiftNoScheduleMode()"
|
||||
[isDragPreview]="true"
|
||||
>
|
||||
@if (dragPreviewLabel()) {
|
||||
<div class="drag-preview-time-badge">
|
||||
{{ dragPreviewLabel() }}
|
||||
</div>
|
||||
<div class="day">
|
||||
@if (day === todayDateStr()) {
|
||||
<mat-icon>wb_sunny</mat-icon>
|
||||
}
|
||||
</schedule-event>
|
||||
<div class="day-num">{{ day | localeDate: 'd' }}</div>
|
||||
<div class="day-day">{{ day | localeDate: 'EEE' }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
#gridContainer
|
||||
class="grid-container"
|
||||
(click)="onGridClick($event)"
|
||||
(mousemove)="onMoveOverGrid($event)"
|
||||
>
|
||||
<!-- Time -->
|
||||
@for (time of times(); track $index) {
|
||||
<div
|
||||
class="time"
|
||||
style="grid-row: {{ $index * FH + 1 }}"
|
||||
>
|
||||
{{ time }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Grid Rows -->
|
||||
@for (row of rowsByNr; track $index) {
|
||||
<div
|
||||
class="row"
|
||||
style="grid-row: {{ row + 1 }}"
|
||||
></div>
|
||||
}
|
||||
|
||||
<!-- Grid Cols -->
|
||||
@for (day of daysToShow(); track $index) {
|
||||
<div
|
||||
class="col"
|
||||
[attr.data-day]="day"
|
||||
style="grid-column: {{ $index + 2 }}; grid-row: 1 / span {{
|
||||
($index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart()) - 1
|
||||
}}"
|
||||
></div>
|
||||
<div
|
||||
class="col end-of-day"
|
||||
[attr.data-day]="day"
|
||||
style="grid-column: {{ $index + 2 }}; grid-row: {{
|
||||
$index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart()
|
||||
}} / span {{
|
||||
totalRows -
|
||||
($index === 0 && currentTimeRow() !== null
|
||||
? currentTimeRow()!
|
||||
: endOfDayColRowStart())
|
||||
}}"
|
||||
></div>
|
||||
}
|
||||
|
||||
<!-- Work Start and End -->
|
||||
@if (workStartEnd()) {
|
||||
<div
|
||||
id="work-start"
|
||||
class="work-start"
|
||||
style="grid-row: {{ workStartEnd()!.workStartRow }}"
|
||||
>
|
||||
<div>{{ T.F.SCHEDULE.START | translate }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="work-end"
|
||||
style="grid-row: {{ workStartEnd()!.workEndRow }}"
|
||||
>
|
||||
<div>{{ T.F.SCHEDULE.END | translate }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (currentTimeRow() !== null) {
|
||||
<div
|
||||
id="current-time"
|
||||
class="current-time"
|
||||
style="grid-column: 2; grid-row: {{ currentTimeRow() }} / span 1"
|
||||
>
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Events -->
|
||||
@for (ev of safeEvents(); track ev.id) {
|
||||
@if (isDraggableSE(ev)) {
|
||||
<schedule-event
|
||||
class="draggable"
|
||||
[cdkDragData]="ev"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
(cdkDragStarted)="dragStarted($event)"
|
||||
(cdkDragReleased)="dragReleased($event)"
|
||||
[cdkDragStartDelay]="IS_TOUCH_PRIMARY ? 75 : 0"
|
||||
[event]="ev"
|
||||
></schedule-event>
|
||||
} @else {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
[cdkDragDisabled]="true"
|
||||
></schedule-event>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Excess tasks planned for day -->
|
||||
@for (beyondBudgetDay of safeBeyondBudget(); track i; let i = $index) {
|
||||
@if (beyondBudgetDay.length > 0) {
|
||||
<div
|
||||
class="excess-entries"
|
||||
style="grid-column: {{ i + 2 }}"
|
||||
>
|
||||
<div
|
||||
class="excess-entries-header"
|
||||
[matTooltipPosition]="'above'"
|
||||
[matTooltip]="
|
||||
'Tasks planned for day, but that are beyond the available time budget'
|
||||
"
|
||||
>
|
||||
<mat-icon>hourglass_disabled</mat-icon>
|
||||
{{ beyondBudgetDay.length }}
|
||||
</div>
|
||||
@for (ev of beyondBudgetDay; track ev.id) {
|
||||
@if (isDraggableSE(ev)) {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
class="draggable"
|
||||
[cdkDragData]="ev"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
(cdkDragStarted)="dragStarted($event)"
|
||||
(cdkDragReleased)="dragReleased($event)"
|
||||
[cdkDragStartDelay]="IS_TOUCH_PRIMARY ? DRAG_DELAY_FOR_TOUCH : 0"
|
||||
></schedule-event>
|
||||
} @else {
|
||||
<schedule-event
|
||||
[event]="ev"
|
||||
[cdkDragDisabled]="true"
|
||||
></schedule-event>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (
|
||||
(!isTaskDragActive() || isCtrlPressed()) && newTaskPlaceholder();
|
||||
as newTaskPlaceholder
|
||||
) {
|
||||
<create-task-placeholder
|
||||
[isEditMode]="isCreateTaskActive()"
|
||||
[time]="newTaskPlaceholder.time"
|
||||
[date]="newTaskPlaceholder.date"
|
||||
(editEnd)="isCreateTaskActive.set(false); this.newTaskPlaceholder.set(null)"
|
||||
[style]="newTaskPlaceholder.style"
|
||||
[style.opacity]="isCtrlPressed() ? 1 : null"
|
||||
>
|
||||
</create-task-placeholder>
|
||||
}
|
||||
|
||||
@if (isDragging() && currentDragEvent() && dragPreviewStyle()) {
|
||||
<schedule-event
|
||||
class="custom-drag-preview"
|
||||
[event]="currentDragEvent()!"
|
||||
[style]="dragPreviewStyle()!"
|
||||
[class.isShiftInsertPreview]="dragPreviewContext()?.kind === 'shift-task'"
|
||||
[class.isScheduleForDay]="isShiftNoScheduleMode()"
|
||||
[isDragPreview]="true"
|
||||
>
|
||||
@if (dragPreviewLabel()) {
|
||||
<div class="drag-preview-time-badge">
|
||||
{{ dragPreviewLabel() }}
|
||||
</div>
|
||||
}
|
||||
</schedule-event>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showShiftKeyInfo()) {
|
||||
|
|
|
|||
|
|
@ -18,33 +18,6 @@
|
|||
|
||||
// Enable horizontal scroll when viewport is too small for 7 days
|
||||
&[data-horizontal-scroll] {
|
||||
.horizontal-scroll-wrapper {
|
||||
overflow-x: scroll; // Always show horizontal scrollbar
|
||||
overflow-y: visible; // Allow vertical content to extend
|
||||
|
||||
// Match app's standard scrollbar styling
|
||||
scrollbar-width: 4px;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px; // Slightly taller than standard for horizontal
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 16px;
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.week-header {
|
||||
// Remove left/right constraints to allow horizontal scrolling
|
||||
left: auto;
|
||||
|
|
|
|||
|
|
@ -33,9 +33,6 @@ import { formatMonthDay } from '../../../util/format-month-day.util';
|
|||
import { ScheduleWeekDragService } from './schedule-week-drag.service';
|
||||
import { calculatePlaceholderForGridMove } from './schedule-week-placeholder.util';
|
||||
import { truncate } from '../../../util/truncate';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { debounceTime, map, startWith } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
const D_HOURS = 24;
|
||||
|
||||
|
|
@ -61,7 +58,7 @@ const D_HOURS = 24;
|
|||
'[class.is-not-dragging]': '!isDragging()',
|
||||
'[class.is-resizing-event]': 'isAnyEventResizing()',
|
||||
'[class]': 'dragEventTypeClass()',
|
||||
'[attr.data-horizontal-scroll]': 'shouldEnableHorizontalScroll()',
|
||||
'[attr.data-horizontal-scroll]': 'isHorizontalScrollMode() || null',
|
||||
},
|
||||
})
|
||||
export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
|
@ -69,21 +66,8 @@ export class ScheduleWeekComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
private _dateTimeFormatService = inject(DateTimeFormatService);
|
||||
private _translateService = inject(TranslateService);
|
||||
|
||||
private _windowSize = toSignal(
|
||||
fromEvent(window, 'resize').pipe(
|
||||
startWith({ width: window.innerWidth }),
|
||||
debounceTime(50),
|
||||
map(() => ({ width: window.innerWidth })),
|
||||
),
|
||||
{ initialValue: { width: window.innerWidth } },
|
||||
);
|
||||
|
||||
shouldEnableHorizontalScroll = computed(() => {
|
||||
// Enable scroll when viewport is smaller than what's needed for 7 days
|
||||
return this._windowSize().width < 1900;
|
||||
});
|
||||
|
||||
isInPanel = input<boolean>(false);
|
||||
isHorizontalScrollMode = input<boolean>(false);
|
||||
events = input<ScheduleEvent[] | null>([]);
|
||||
beyondBudget = input<ScheduleEvent[][] | null>([]);
|
||||
daysToShow = input<string[]>([]);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scroll-wrapper">
|
||||
<div
|
||||
class="scroll-wrapper"
|
||||
[attr.data-horizontal-scroll]="shouldEnableHorizontalScroll() || null"
|
||||
>
|
||||
@if (isMonthView()) {
|
||||
<schedule-month
|
||||
[events]="events()"
|
||||
|
|
@ -46,6 +49,7 @@
|
|||
[workStartEnd]="workStartEnd() || null"
|
||||
[currentTimeRow]="currentTimeRow()"
|
||||
[todayDateStr]="_todayDateStr()"
|
||||
[isHorizontalScrollMode]="shouldEnableHorizontalScroll()"
|
||||
></schedule-week>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,34 @@
|
|||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Enable horizontal scroll when viewport is too small for 7 days
|
||||
&[data-horizontal-scroll] {
|
||||
overflow-x: scroll; // Show horizontal scrollbar when needed
|
||||
|
||||
// Match app's standard scrollbar styling
|
||||
scrollbar-width: 4px;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 8px; // Slightly taller for horizontal visibility
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 16px;
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,16 @@ export class ScheduleComponent {
|
|||
{ initialValue: { width: window.innerWidth, height: window.innerHeight } },
|
||||
);
|
||||
|
||||
shouldEnableHorizontalScroll = computed(() => {
|
||||
const selectedView = this._currentTimeViewMode();
|
||||
// Only enable horizontal scroll for week view when viewport is narrow
|
||||
if (selectedView !== 'week') {
|
||||
return false;
|
||||
}
|
||||
// Enable scroll when viewport is smaller than what's needed for 7 days
|
||||
return this._windowSize().width < 1900;
|
||||
});
|
||||
|
||||
private _daysToShowCount = computed(() => {
|
||||
const size = this._windowSize();
|
||||
const selectedView = this._currentTimeViewMode();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue