From ec0b69b5796e5656fd13a6af2e8df86291927b4c Mon Sep 17 00:00:00 2001 From: Chris Malloy Date: Mon, 14 Oct 2024 17:52:46 -0300 Subject: [PATCH] Hide tabs that don't fit --- .../action-list/action-list.component.ts | 13 +- src/app/component/tabs/tabs.component.html | 7 +- src/app/component/tabs/tabs.component.ts | 134 ++++++++++++++++-- src/app/service/config.service.ts | 5 + src/theme/common.scss | 16 ++- src/theme/mobile.scss | 21 --- 6 files changed, 155 insertions(+), 41 deletions(-) diff --git a/src/app/component/action/action-list/action-list.component.ts b/src/app/component/action/action-list/action-list.component.ts index 05947e3dc..28aabc6b1 100644 --- a/src/app/component/action/action-list/action-list.component.ts +++ b/src/app/component/action/action-list/action-list.component.ts @@ -11,7 +11,7 @@ import { ViewChild, ViewContainerRef } from '@angular/core'; -import { debounce, defer, delay } from 'lodash-es'; +import { defer } from 'lodash-es'; import { Subscription } from 'rxjs'; import { Ref, writeRef } from '../../../model/ref'; import { Action } from '../../../model/tag'; @@ -83,11 +83,10 @@ export class ActionListComponent implements AfterViewInit { this.measureVisible(); } - measureVisible = debounce(() => { + measureVisible() { if (!this.actions) return; - defer(() => this.hiddenActions = this.actions - this.visible); - delay(() => this.hiddenActions = this.actions - this.visible, 500); - }, 400); + this.hiddenActions = this.actions - this.visible; + } @memo get actions() { @@ -98,8 +97,8 @@ export class ActionListComponent implements AfterViewInit { get actionWidths() { const el = this.el.nativeElement; const result: number[] = []; - for (let i = 0; i < el!.children.length; i++) { - const e = el.parentElement!.children[i] as HTMLElement; + for (let i = 0; i < el.children.length; i++) { + const e = el.children[i] as HTMLElement; const s = getComputedStyle(e); result.push(e.offsetWidth + parseInt(s.marginLeft) + parseInt(s.marginRight)); } diff --git a/src/app/component/tabs/tabs.component.html b/src/app/component/tabs/tabs.component.html index 4092e7c88..1ad361e98 100644 --- a/src/app/component/tabs/tabs.component.html +++ b/src/app/component/tabs/tabs.component.html @@ -1,9 +1,10 @@ -
+
diff --git a/src/app/component/tabs/tabs.component.ts b/src/app/component/tabs/tabs.component.ts index abcfab263..d70c88273 100644 --- a/src/app/component/tabs/tabs.component.ts +++ b/src/app/component/tabs/tabs.component.ts @@ -1,6 +1,16 @@ -import { AfterViewInit, Component, ContentChildren, ElementRef, HostBinding, QueryList } from '@angular/core'; -import { NavigationEnd, Router, RouterLink } from '@angular/router'; -import { filter } from 'rxjs'; +import { + AfterViewInit, + Component, + ContentChildren, + ElementRef, + HostBinding, + HostListener, + QueryList +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { defer } from 'lodash-es'; +import { ConfigService } from '../../service/config.service'; +import { memo, MemoCache } from '../../util/memo'; @Component({ selector: 'app-tabs', @@ -17,23 +27,43 @@ export class TabsComponent implements AfterViewInit { options: string[] = []; map = new Map(); + hidden = 0; + + private resizeObserver = window.ResizeObserver && new ResizeObserver(() => this.onResize()) || undefined; constructor( - private router: Router - ) { - this.router.events.pipe( - filter(event => event instanceof NavigationEnd) - ).subscribe(() => this.updateTabs()); - } + private config: ConfigService, + private el: ElementRef, + ) { } ngAfterViewInit() { this.updateTabs(); this.anchors.changes.subscribe(value => { this.updateTabs(); }); + defer(() => this.resizeObserver?.observe(this.el.nativeElement!.parentElement!)); + } + + @HostBinding('class.floating-tabs') + get floatingTabs() { + return this.config.mini || this.hidden > 0 && this.hidden === this.options.length; + } + + @HostListener('window:resize') + onResize() { + if (!this.options.length) return; + this.measureVisible(); + } + + measureVisible() { + if (!this.options.length) return; + this.hidden = this.options.length - this.visible; + this.hideTabs(); } updateTabs() { + MemoCache.clear(this); + this.hidden = 0; this.options = []; this.map.clear(); const tabs = this.anchors.toArray(); @@ -45,6 +75,91 @@ export class TabsComponent implements AfterViewInit { this.options.push(value); this.map.set(value, tabs.indexOf(t)); } + defer(() => this.onResize()); + } + + hideTabs() { + const tabs = this.anchors.toArray(); + let i = tabs.length - 2; + for (const t of tabs) { + const el = t.nativeElement as HTMLAnchorElement; + if (el.tagName !== 'A') continue; + if (el.classList.contains('logo')) continue; + if (el.classList.contains('current-tab')) { + el.style.display = 'inline-block'; + continue; + } + el.style.display = i > this.hidden ? 'inline-block' : 'none'; + i--; + } + } + + @memo + get tabWidths() { + const result: number[] = []; + const tabs = this.anchors.toArray(); + for (const t of tabs) { + const el = t.nativeElement as HTMLAnchorElement; + if (el.tagName !== 'A') continue; + if (el.classList.contains('logo')) continue; + result.push(el.offsetWidth + 10); + } + return result; + } + + get currentTabWidth() { + const el = this.el.nativeElement; + for (let i = 0; i < el.children.length; i++) { + const e = el.children[i] as HTMLElement; + if (!e.classList.contains('current-tab')) continue; + return e.offsetWidth + 10; + } + return 0; + } + + get childWidths() { + const el = this.el.nativeElement; + const result: number[] = []; + let mobileSelect = false; + for (let i = 0; i < el.children.length; i++) { + const e = el.children[i] as HTMLElement; + if (e.tagName === 'A' && !e.classList.contains('logo')) continue; + if (this.config.mobile) { + if (e.tagName === 'H5') continue; + if (e.classList.contains('logo')) continue; + } + if (e.classList.contains('mobile-tab-select')) mobileSelect = true; + result.push(e.offsetWidth); + } + if (!mobileSelect) { + result.push(52); + } + return result; + } + + get visible() { + const current = this.currentTabWidth; + if (!current) return this.options.length; + if (this.floatingTabs) return 0; + const el = this.el.nativeElement; + const width = el.offsetWidth - 16; + let result = 1; + let childWidth = current + this.childWidths.reduce((a, b) => a + b); + if (childWidth > width) return 0; + let skipped = false; + for (const w of this.tabWidths) { + if (!skipped && w === current) { + skipped = true; + continue; + } + childWidth += w; + if (childWidth < width) { + result++; + } else { + return result; + } + } + return this.options.length; } nav(select: HTMLSelectElement) { @@ -52,6 +167,7 @@ export class TabsComponent implements AfterViewInit { this.routerLinks.get(this.map.get(select.value)!)?.onClick(0, false, false, false, false); } select.selectedIndex = 0; + this.measureVisible(); } } diff --git a/src/app/service/config.service.ts b/src/app/service/config.service.ts index 7367aae40..7a710d69d 100644 --- a/src/app/service/config.service.ts +++ b/src/app/service/config.service.ts @@ -41,6 +41,7 @@ export class ConfigService { */ prefetch = isDevMode(); + miniWidth = 380; mobileWidth = 740; tabletWidth = 948; hugeWidth = 1500; @@ -71,6 +72,10 @@ export class ConfigService { ); } + get mini() { + return window.innerWidth <= this.miniWidth; + } + get mobile() { return window.innerWidth <= this.mobileWidth; } diff --git a/src/theme/common.scss b/src/theme/common.scss index c07fd3434..cd23f0093 100644 --- a/src/theme/common.scss +++ b/src/theme/common.scss @@ -790,7 +790,7 @@ video.qr-preview { &.empty > a:not(.logo), &.empty > app-mobile-tab-select { - mask-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0))); + mask-image: gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0))); } h5 { @@ -800,6 +800,19 @@ video.qr-preview { overflow: hidden; } + &.floating-tabs { + &>a:not(.logo) { + display: none !important; + } + .current-tab { + display: block; + border: 1px solid var(--border) !important; + margin: 4px; + flex-basis: 100%; + border-radius: 4px; + } + } + .mobile-tab-select { margin: 0 4px; select { @@ -844,6 +857,7 @@ video.qr-preview { &.current-tab { color: var(--active) !important; + display: inline-block !important; background-color: var(--bg); border-top: 1px solid var(--border); border-left: 1px solid var(--border); diff --git a/src/theme/mobile.scss b/src/theme/mobile.scss index 27dcff3b5..ca56374a4 100644 --- a/src/theme/mobile.scss +++ b/src/theme/mobile.scss @@ -2,8 +2,6 @@ $mini-width: 380px; // Mobile: Change tabs and forms $mobile-width: 740px; -// Mobile: Change tabs and forms -$settings-tabs-width: 1024px; // Tablet: Move sidebar above $tablet-width: 948px; @@ -11,10 +9,6 @@ $tablet-width: 948px; /* Tablet and Bigger Screens */ @media (min-width: $mobile-width) { - .mobile-tab-select { - display: none; - } - .toggle { &.actions-toggle, &.view, &.threads, &.comments { display: none; @@ -119,18 +113,6 @@ $tablet-width: 948px; } } -/* Special width for settings tabs */ -@media (max-width: $settings-tabs-width) { - .settings .tabs { - & > a:not(.current-tab):not(.logo) { - display: none; - } - .mobile-tab-select { - display: block; - } - } -} - /* Phone Screens */ @media (max-width: $mobile-width) { input[type="color"], @@ -188,9 +170,6 @@ $tablet-width: 948px; h5 { flex-basis: 100%; } - & > a:not(.current-tab):not(.logo) { - display: none; - } } .editor, .summary-box {