mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
23 changed files with 486 additions and 101 deletions
@ -0,0 +1,30 @@ |
|||||
|
<ng-template #dropdownTemplate> |
||||
|
<a |
||||
|
class="dropdown-item" |
||||
|
[attr.disabled]="disabled" |
||||
|
[class.disabled]="disabled" |
||||
|
[confirmRememberKey]="confirmRememberKey" |
||||
|
[confirmText]="confirmText | sqxTranslate" |
||||
|
[confirmTitle]="confirmTitle | sqxTranslate" |
||||
|
(sqxConfirmClick)="action.emit()" |
||||
|
[title]="tooltip | sqxTranslate"> |
||||
|
{{ menuLabel || label | sqxTranslate }} |
||||
|
</a> |
||||
|
</ng-template> |
||||
|
|
||||
|
<button |
||||
|
class="btn btn-outline-secondary" |
||||
|
[class.btn-sm]="small" |
||||
|
[confirmRememberKey]="confirmRememberKey" |
||||
|
[confirmText]="confirmText | sqxTranslate" |
||||
|
[confirmTitle]="confirmTitle | sqxTranslate" |
||||
|
[disabled]="disabled" |
||||
|
(sqxConfirmClick)="action.emit()" |
||||
|
[tabIndex]="tabIndex" |
||||
|
[title]="tooltip | sqxTranslate"> |
||||
|
@if (icon) { |
||||
|
<i class="icon-{{ icon }}"></i> |
||||
|
} |
||||
|
|
||||
|
{{ label | sqxTranslate }} |
||||
|
</button> |
||||
@ -0,0 +1,8 @@ |
|||||
|
@import 'mixins'; |
||||
|
@import 'vars'; |
||||
|
|
||||
|
.btn-sm { |
||||
|
i { |
||||
|
font-size: $font-smallest; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, TemplateRef, ViewChild } from '@angular/core'; |
||||
|
import { ConfirmClickDirective } from './forms/confirm-click.directive'; |
||||
|
import { TooltipDirective } from './modals/tooltip.directive'; |
||||
|
import { TranslatePipe } from './pipes/translate.pipe'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-menu-item', |
||||
|
styleUrls: ['./menu-item.component.scss'], |
||||
|
templateUrl: './menu-item.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
imports: [ |
||||
|
ConfirmClickDirective, |
||||
|
TooltipDirective, |
||||
|
TranslatePipe, |
||||
|
], |
||||
|
}) |
||||
|
export class MenuItemComponent { |
||||
|
@Input() |
||||
|
public label = ''; |
||||
|
|
||||
|
@Input() |
||||
|
public menuLabel = ''; |
||||
|
|
||||
|
@Input() |
||||
|
public icon = ''; |
||||
|
|
||||
|
@Input() |
||||
|
public tooltip = ''; |
||||
|
|
||||
|
@Input({ transform: numberAttribute }) |
||||
|
public tabIndex = -1; |
||||
|
|
||||
|
@Input({ transform: booleanAttribute }) |
||||
|
public disabled = false; |
||||
|
|
||||
|
@Input({ transform: booleanAttribute }) |
||||
|
public small = false; |
||||
|
|
||||
|
@Input() |
||||
|
public confirmRememberKey = ''; |
||||
|
|
||||
|
@Input() |
||||
|
public confirmTitle = ''; |
||||
|
|
||||
|
@Input() |
||||
|
public confirmText = ''; |
||||
|
|
||||
|
@Output() |
||||
|
public action = new EventEmitter(); |
||||
|
|
||||
|
@ViewChild('dropdownTemplate', { static: true }) |
||||
|
template!: TemplateRef<any>; |
||||
|
|
||||
|
public get showInDropdown() { |
||||
|
return this.label || this.menuLabel; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<div class="d-flex flex-nowrap relative" #container [class.gap-1]="small" [class.gap-2]="!small" [class.justify-content-end]="isRightAligned"> |
||||
|
@if (overflowMenuItems) { |
||||
|
<button |
||||
|
class="btn btn-text-secondary" |
||||
|
#buttonOptions |
||||
|
attr.aria-label="{{ 'common.options' | sqxTranslate }}" |
||||
|
[class.btn-sm]="small" |
||||
|
[class.order-1]="isRightAligned" |
||||
|
(click)="overflowDropdown.toggle()" |
||||
|
sqxStopClick |
||||
|
type="button"> |
||||
|
<i class="icon-dots"></i> |
||||
|
</button> |
||||
|
|
||||
|
<sqx-dropdown-menu |
||||
|
[position]="isRightAligned ? 'bottom-end' : 'bottom-start'" |
||||
|
scrollY="true" |
||||
|
[sqxAnchoredTo]="buttonOptions" |
||||
|
*sqxModal="overflowDropdown; closeAlways: true"> |
||||
|
@for (item of overflowMenuItems; track item) { |
||||
|
<ng-container *ngTemplateOutlet="item.template"></ng-container> |
||||
|
} |
||||
|
</sqx-dropdown-menu> |
||||
|
} |
||||
|
|
||||
|
<div class="d-flex flex-nowrap" #menu [class.gap-1]="small" [class.gap-2]="!small" [class.overflown-right]="!!overflowMenuItems && isRightAligned" [class.overflown-left]="!!overflowMenuItems && !isRightAligned"> |
||||
|
<ng-content /> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,24 @@ |
|||||
|
@import 'mixins'; |
||||
|
@import 'vars'; |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.overflown-left { |
||||
|
sqx-menu-item { |
||||
|
order: 5; |
||||
|
visibility: hidden; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.overflown-right { |
||||
|
sqx-menu-item { |
||||
|
order: -5; |
||||
|
visibility: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.btn-sm { |
||||
|
i { |
||||
|
font-size: $font-smallest; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
import { NgTemplateOutlet } from '@angular/common'; |
||||
|
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, Input, QueryList, ViewChild } from '@angular/core'; |
||||
|
import { ModalModel, ResizeListener, ResizeService, Subscriptions } from '@app/framework/internal'; |
||||
|
import { DropdownMenuComponent } from './dropdown-menu.component'; |
||||
|
import { MenuItemComponent } from './menu-item.component'; |
||||
|
import { ModalPlacementDirective } from './modals/modal-placement.directive'; |
||||
|
import { ModalDirective } from './modals/modal.directive'; |
||||
|
import { TranslatePipe } from './pipes/translate.pipe'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-menu', |
||||
|
styleUrls: ['./menu.component.scss'], |
||||
|
templateUrl: './menu.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
imports: [ |
||||
|
DropdownMenuComponent, |
||||
|
ModalDirective, |
||||
|
ModalPlacementDirective, |
||||
|
NgTemplateOutlet, |
||||
|
TranslatePipe, |
||||
|
], |
||||
|
}) |
||||
|
export class MenuComponent implements AfterViewInit, ResizeListener { |
||||
|
private readonly subscriptions = new Subscriptions(); |
||||
|
private measuredContainer?: number; |
||||
|
private measuredMenu?: number; |
||||
|
|
||||
|
@Input() |
||||
|
public alignment: 'left' | 'right' = 'left'; |
||||
|
|
||||
|
@Input({ transform: booleanAttribute }) |
||||
|
public small = false; |
||||
|
|
||||
|
@Input({ transform: booleanAttribute }) |
||||
|
public showCustom = true; |
||||
|
|
||||
|
@ViewChild('container', { static: true }) |
||||
|
public container!: ElementRef<HTMLDivElement>; |
||||
|
|
||||
|
@ViewChild('menu', { static: true }) |
||||
|
public menu!: ElementRef<HTMLDivElement>; |
||||
|
|
||||
|
@ContentChildren(MenuItemComponent) |
||||
|
public menuItems!: QueryList<MenuItemComponent>; |
||||
|
|
||||
|
public overflowMenuItems: MenuItemComponent[] | null = null; |
||||
|
public overflowDropdown = new ModalModel(); |
||||
|
|
||||
|
public get isRightAligned() { |
||||
|
return this.alignment === 'right'; |
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
private readonly resizeService: ResizeService, |
||||
|
private readonly changeDetector: ChangeDetectorRef, |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
this.subscriptions.add(this.resizeService.listen(this.container.nativeElement, this)); |
||||
|
this.subscriptions.add(this.resizeService.listen(this.menu.nativeElement, this)); |
||||
|
} |
||||
|
|
||||
|
public onResize(rect: DOMRect, element: Element): void { |
||||
|
if (element === this.container.nativeElement) { |
||||
|
this.measuredContainer = rect.width; |
||||
|
} else { |
||||
|
this.measuredMenu = Math.max(rect.width, element.scrollWidth); |
||||
|
} |
||||
|
|
||||
|
if (!this.measuredContainer || !this.measuredMenu) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const isOverlapping = this.measuredMenu > this.measuredContainer; |
||||
|
this.overflowMenuItems = isOverlapping ? this.menuItems.toArray().filter(x => x.showInDropdown) : null; |
||||
|
this.changeDetector.detectChanges(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; |
||||
|
import { LocalizerService, ResizeService } from '@app/framework/internal'; |
||||
|
import { MenuItemComponent } from './menu-item.component'; |
||||
|
import { MenuComponent } from './menu.component'; |
||||
|
import { RootViewComponent } from './modals/root-view.component'; |
||||
|
|
||||
|
export default { |
||||
|
title: 'Framework/Menu', |
||||
|
component: MenuComponent, |
||||
|
args: { |
||||
|
small: false, |
||||
|
}, |
||||
|
argTypes: { |
||||
|
alignment: { |
||||
|
control: 'radio', |
||||
|
options: [ |
||||
|
'left', |
||||
|
'right', |
||||
|
], |
||||
|
}, |
||||
|
small: { |
||||
|
control: 'boolean', |
||||
|
}, |
||||
|
}, |
||||
|
decorators: [ |
||||
|
moduleMetadata({ |
||||
|
imports: [ |
||||
|
BrowserAnimationsModule, |
||||
|
MenuItemComponent, |
||||
|
RootViewComponent, |
||||
|
], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: LocalizerService, |
||||
|
useFactory: () => new LocalizerService({}), |
||||
|
}, |
||||
|
ResizeService, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
} as Meta; |
||||
|
|
||||
|
type Story = StoryObj<MenuComponent>; |
||||
|
|
||||
|
export const Simple: Story = { |
||||
|
render: args => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<sqx-root-view> |
||||
|
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;"> |
||||
|
<sqx-menu [alignment]="alignment" [items]="items" [small]="small"> |
||||
|
<sqx-menu-item label="Menu Item1" /> |
||||
|
<sqx-menu-item label="Menu Item2" icon="close" /> |
||||
|
<sqx-menu-item label="Menu Item3" disabled /> |
||||
|
</sqx-menu> |
||||
|
</div> |
||||
|
</sqx-root-view> |
||||
|
`,
|
||||
|
}), |
||||
|
}; |
||||
|
|
||||
|
export const Overlap: Story = { |
||||
|
render: args => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<sqx-root-view> |
||||
|
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;"> |
||||
|
<sqx-menu [alignment]="alignment" [items]="items" [small]="small"> |
||||
|
<sqx-menu-item label="Menu Item1" /> |
||||
|
<sqx-menu-item label="Menu Item2" icon="close" /> |
||||
|
<sqx-menu-item label="Menu Item3" disabled /> |
||||
|
<sqx-menu-item label="Menu Item4" /> |
||||
|
<sqx-menu-item label="Menu Item5" /> |
||||
|
</sqx-menu> |
||||
|
</div> |
||||
|
</sqx-root-view> |
||||
|
`,
|
||||
|
}), |
||||
|
}; |
||||
|
|
||||
|
export const Custom: Story = { |
||||
|
render: args => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<sqx-root-view> |
||||
|
<div style="width: 500px; padding: 10px; border: 1px solid #ddd;"> |
||||
|
<sqx-menu [alignment]="alignment" [items]="items" [small]="small"> |
||||
|
<sqx-menu-item label="Menu Item1" /> |
||||
|
|
||||
|
<button class="btn btn-primary">Custom 1</button> |
||||
|
<button class="btn btn-primary">Custom 2</button> |
||||
|
|
||||
|
<sqx-menu-item label="Menu Item2" /> |
||||
|
</sqx-menu> |
||||
|
</div> |
||||
|
</sqx-root-view> |
||||
|
`,
|
||||
|
}), |
||||
|
}; |
||||
|
|
||||
|
export const CustomOverlap: Story = { |
||||
|
render: args => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<sqx-root-view> |
||||
|
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;"> |
||||
|
<sqx-menu [alignment]="alignment" [items]="items" [small]="small"> |
||||
|
<sqx-menu-item label="Menu Item1" /> |
||||
|
<sqx-menu-item label="Menu Item2" icon="close" /> |
||||
|
<sqx-menu-item label="Menu Item3" disabled /> |
||||
|
|
||||
|
<button class="btn btn-primary">Custom 1</button> |
||||
|
<button class="btn btn-primary">Custom 2</button> |
||||
|
|
||||
|
<sqx-menu-item label="Menu Item4" /> |
||||
|
<sqx-menu-item label="Menu Item5" /> |
||||
|
</sqx-menu> |
||||
|
</div> |
||||
|
</sqx-root-view> |
||||
|
`,
|
||||
|
}), |
||||
|
}; |
||||
Loading…
Reference in new issue