diff --git a/src/app/components/container/simulator.component.ts b/src/app/components/container/simulator.component.ts index 0aa06eb..fa06ed4 100644 --- a/src/app/components/container/simulator.component.ts +++ b/src/app/components/container/simulator.component.ts @@ -3,6 +3,7 @@ import { PcSimulatorComponent } from '../simulators/pc-simulator/pc-simulator.co import { Engine, Screen, ScreenType } from '@/app/core/models'; import { MobileSimulatorComponent } from '@/app/components/simulators/mobile-simulator/mobile-simulator.component'; import { NgTemplateOutlet } from '@angular/common'; +import { ResponsiveSimulator } from '@/app/components/simulators/responsive-simulator/responsive-simulator.component'; @Component({ selector: 'app-simulator', @@ -16,14 +17,14 @@ import { NgTemplateOutlet } from '@angular/common'; } @else { - + - + } `, styles: [``], standalone: true, - imports: [PcSimulatorComponent, MobileSimulatorComponent, NgTemplateOutlet], + imports: [PcSimulatorComponent, MobileSimulatorComponent, NgTemplateOutlet, ResponsiveSimulator], changeDetection: ChangeDetectionStrategy.Default }) export class SimulatorComponent { diff --git a/src/app/components/icons/dragmove.ts b/src/app/components/icons/dragmove.ts new file mode 100644 index 0000000..9e40ed6 --- /dev/null +++ b/src/app/components/icons/dragmove.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-drag-move-svg', + standalone: true, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DragMoveSvg { + @Input() width: string | number = '1em'; + + @Input() height: string | number = '1em'; +} diff --git a/src/app/components/icons/icon.register.ts b/src/app/components/icons/icon.register.ts index 60846d2..e60e37a 100644 --- a/src/app/components/icons/icon.register.ts +++ b/src/app/components/icons/icon.register.ts @@ -32,6 +32,9 @@ import { CloneSvg } from '@/app/components/icons/clone'; import { ContainerSvg } from '@/app/components/icons/container'; import { RemoveSvg } from '@/app/components/icons/remove'; import { FlipSvg } from '@/app/components/icons/flip'; +import { RecoverSvg } from '@/app/components/icons/recover'; +import { MenuSvg } from '@/app/components/icons/menu'; +import { DragMoveSvg } from '@/app/components/icons/dragmove'; export class IconRegister extends IconFactory { constructor() { @@ -69,5 +72,8 @@ export class IconRegister extends IconFactory { this.register(IconType.Container, ContainerSvg); this.register(IconType.Remove, RemoveSvg); this.register(IconType.Flip, FlipSvg); + this.register(IconType.Recover, RecoverSvg); + this.register(IconType.Menu, MenuSvg); + this.register(IconType.DragMove, DragMoveSvg); } } diff --git a/src/app/components/icons/icon.type.ts b/src/app/components/icons/icon.type.ts index 4d258cd..e8b5e88 100644 --- a/src/app/components/icons/icon.type.ts +++ b/src/app/components/icons/icon.type.ts @@ -31,5 +31,8 @@ export enum IconType { Clone = 'Clone', Container = 'Container', Remove = 'Remove', - Flip = 'Flip' + Flip = 'Flip', + Recover = 'Recover', + Menu = 'Menu', + DragMove = 'DragMove' } diff --git a/src/app/components/icons/menu.ts b/src/app/components/icons/menu.ts new file mode 100644 index 0000000..80e9795 --- /dev/null +++ b/src/app/components/icons/menu.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-menu-svg', + standalone: true, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MenuSvg { + @Input() width: string | number = '1em'; + + @Input() height: string | number = '1em'; +} diff --git a/src/app/components/icons/recover.ts b/src/app/components/icons/recover.ts new file mode 100644 index 0000000..0a80fe7 --- /dev/null +++ b/src/app/components/icons/recover.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-recover-svg', + standalone: true, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RecoverSvg { + @Input() width: string | number = '1em'; + + @Input() height: string | number = '1em'; +} diff --git a/src/app/components/simulators/responsive-simulator/resize-handler.component.ts b/src/app/components/simulators/responsive-simulator/resize-handler.component.ts new file mode 100644 index 0000000..4010d6a --- /dev/null +++ b/src/app/components/simulators/responsive-simulator/resize-handler.component.ts @@ -0,0 +1,46 @@ +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { usePrefix } from '../../../utils'; +import { Engine } from '../../../core/models'; +import { AttributeDirective } from '../../../directive'; +import { NgClass } from '@angular/common'; + +export enum ResizeHandleType { + Resize = 'RESIZE', + ResizeWidth = 'RESIZE_WIDTH', + ResizeHeight = 'RESIZE_HEIGHT' +} + +@Component({ + selector: 'app-resize-handler', + template: ` +
+ +
+ `, + standalone: true, + imports: [AttributeDirective, NgClass], + styleUrls: ['./responsive-simulator.component.less'], + encapsulation: ViewEncapsulation.None +}) +export class ResizeHandlerComponent implements OnChanges { + prefix = usePrefix('resize-handle'); + + @Input() type: ResizeHandleType; + + attributes: { [p: string]: any }; + + currentClass: { [p: string]: boolean }; + + constructor(private designer: Engine) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.type && changes.type.currentValue) { + this.attributes = { + [this.designer.props.screenResizeHandlerAttrName]: this.type + }; + this.currentClass = { + [`${this.prefix}-${this.type}`]: !!this.type + }; + } + } +} diff --git a/src/app/components/simulators/responsive-simulator/responsive-simulator.component.less b/src/app/components/simulators/responsive-simulator/responsive-simulator.component.less new file mode 100644 index 0000000..849851c --- /dev/null +++ b/src/app/components/simulators/responsive-simulator/responsive-simulator.component.less @@ -0,0 +1,56 @@ +@import '../../../../theme/variables.less'; + +.@{prefix-cls}-responsive-simulator { + background-color: var(--dn-responsive-simulator-bg-color); +} + +.@{prefix-cls}-resize-handle { + position: absolute; + transition: 0.2s all ease-in-out; + box-sizing: border-box; + user-select: none; + bottom: 0; + z-index: 10; + background: var(--dn-resize-handle-bg-color); + color: var(--dn-resize-handle-color); + display: flex; + justify-content: center; + align-items: center; + + &-RESIZE_WIDTH { + top: 0; + bottom: 15px; + cursor: ew-resize; + + svg { + transform-origin: center; + transform: rotate(-90deg); + } + } + + &-RESIZE_HEIGHT { + left: 0; + right: 15px; + cursor: ns-resize; + } + + &-RESIZE { + cursor: nwse-resize; + } + + &-RESIZE_HEIGHT, + &-RESIZE { + height: 15px; + } + + &-RESIZE_WIDTH, + &-RESIZE { + right: 0; + width: 15px; + } + + &:hover { + background: var(--dn-resize-handle-hover-bg-color); + color: var(--dn-resize-handle-hover-color); + } +} diff --git a/src/app/components/simulators/responsive-simulator/responsive-simulator.component.ts b/src/app/components/simulators/responsive-simulator/responsive-simulator.component.ts new file mode 100644 index 0000000..bc7f3cb --- /dev/null +++ b/src/app/components/simulators/responsive-simulator/responsive-simulator.component.ts @@ -0,0 +1,216 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnChanges, + signal, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { usePrefix } from '@/app/utils'; +import { HookService } from '@/app/services/hook.service'; +import { CursorDragType, Engine, Screen } from '@/app/core/models'; +import { + ResizeHandlerComponent, + ResizeHandleType +} from '@/app/components/simulators/responsive-simulator/resize-handler.component'; +import { IconWidget } from '@/app/components/widgets/icon/icon.widget'; +import { DragMoveEvent, DragStartEvent, DragStopEvent } from '@/app/core/events'; +import { calcSpeedFactor, createUniformSpeedAnimation } from '@/app/shared/animation'; +import { fromEvent } from 'rxjs'; + +@Component({ + selector: 'app-responsive-simulator', + template: ` +
+
+
+ + + + + + + + + + +
+
+
+ `, + standalone: true, + imports: [ResizeHandlerComponent, IconWidget], + styleUrls: ['./responsive-simulator.component.less'] +}) +export class ResponsiveSimulator implements OnChanges, AfterViewInit { + @Input() className: string; + + @Input() style: Partial; + + @ViewChild('container') container: ElementRef; + + @ViewChild('content') content: ElementRef; + + prefix = usePrefix('responsive-simulator'); + + currentStyle = signal({ + height: '100%', + width: '100%', + minHeight: '100px', + position: 'relative' + }); + + screen: Screen; + + engine: Engine; + + contentStyle: { [p: string]: any }; + + protected readonly ResizeHandleType = ResizeHandleType; + + constructor( + private hookService: HookService, + private cdr: ChangeDetectorRef + ) { + this.screen = this.hookService.useScreen(); + this.engine = this.hookService.useDesigner(); + + this.contentStyle = { + width: `${this.screen.width == '100%' ? this.screen.width : this.screen.width + 'px'}`, + height: `${this.screen.height == '100%' ? this.screen.height : this.screen.height + 'px'}`, + paddingRight: '15px', + paddingBottom: '15px', + position: 'relative', + boxSizing: 'border-box', + overflow: 'hidden' + }; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.style && changes.style.currentValue) { + this.currentStyle.set({ + height: '100%', + width: '100%', + minHeight: '100px', + position: 'relative', + ...this.style + }); + } + } + + ngAfterViewInit(): void { + useResizeEffect(this.container.nativeElement, this.content.nativeElement, this.engine); + + fromEvent(window, 'mousemove').subscribe(() => { + this.contentStyle = { + width: `${this.screen.width == '100%' ? this.screen.width : this.screen.width + 'px'}`, + height: `${this.screen.height == '100%' ? this.screen.height : this.screen.height + 'px'}`, + paddingRight: '15px', + paddingBottom: '15px', + position: 'relative', + boxSizing: 'border-box', + overflow: 'hidden' + }; + }); + } +} + +export const useResizeEffect = (container: HTMLDivElement, content: HTMLDivElement, engine: Engine) => { + let status: ResizeHandleType = null; + let startX = 0; + let startY = 0; + let startWidth = 0; + let startHeight = 0; + let animationX = null; + let animationY = null; + + const getStyle = (status: ResizeHandleType) => { + if (status === ResizeHandleType.Resize) return 'nwse-resize'; + if (status === ResizeHandleType.ResizeHeight) return 'ns-resize'; + if (status === ResizeHandleType.ResizeWidth) return 'ew-resize'; + return 'nwse-resize'; + }; + + const updateSize = (deltaX: number, deltaY: number) => { + const containerRect = container?.getBoundingClientRect(); + if (status === ResizeHandleType.Resize) { + engine.screen.setSize(startWidth + deltaX, startHeight + deltaY); + container.scrollBy(containerRect.width + deltaX, containerRect.height + deltaY); + } else if (status === ResizeHandleType.ResizeHeight) { + engine.screen.setSize(startWidth, startHeight + deltaY); + container.scrollBy(container.scrollLeft, containerRect.height + deltaY); + } else if (status === ResizeHandleType.ResizeWidth) { + engine.screen.setSize(startWidth + deltaX, startHeight); + container.scrollBy(containerRect.width + deltaX, container.scrollTop); + } + }; + + engine.subscribeTo(DragStartEvent, e => { + if (!engine.workbench.currentWorkspace?.viewport) return; + const target = e.data.target as HTMLElement; + if (target?.closest(`*[${engine.props.screenResizeHandlerAttrName}]`)) { + const rect = content?.getBoundingClientRect(); + if (!rect) return; + status = target.getAttribute(engine.props.screenResizeHandlerAttrName) as ResizeHandleType; + engine.cursor.setStyle(getStyle(status)); + startX = e.data.topClientX; + startY = e.data.topClientY; + startWidth = rect.width; + startHeight = rect.height; + engine.cursor.setDragType(CursorDragType.Resize); + } + }); + engine.subscribeTo(DragMoveEvent, e => { + if (!engine.workbench.currentWorkspace?.viewport) return; + if (!status) return; + const deltaX = e.data.topClientX - startX; + const deltaY = e.data.topClientY - startY; + const containerRect = container?.getBoundingClientRect(); + const distanceX = Math.floor(containerRect.right - e.data.topClientX); + const distanceY = Math.floor(containerRect.bottom - e.data.topClientY); + const factorX = calcSpeedFactor(distanceX, 10); + const factorY = calcSpeedFactor(distanceY, 10); + updateSize(deltaX, deltaY); + if (distanceX <= 10) { + if (!animationX) { + animationX = createUniformSpeedAnimation(1000 * factorX, delta => { + updateSize(deltaX + delta, deltaY); + }); + } + } else { + if (animationX) { + animationX = animationX(); + } + } + + if (distanceY <= 10) { + if (!animationY) { + animationY = createUniformSpeedAnimation(300 * factorY, delta => { + updateSize(deltaX, deltaY + delta); + }); + } + } else { + if (animationY) { + animationY = animationY(); + } + } + }); + engine.subscribeTo(DragStopEvent, () => { + if (!status) return; + status = null; + engine.cursor.setStyle(''); + engine.cursor.setDragType(CursorDragType.Move); + if (animationX) { + animationX = animationX(); + } + if (animationY) { + animationY = animationY(); + } + }); +}; diff --git a/src/app/components/widgets/designer-tool/designer-tool.widget.html b/src/app/components/widgets/designer-tool/designer-tool.widget.html index 6680057..370e01b 100644 --- a/src/app/components/widgets/designer-tool/designer-tool.widget.html +++ b/src/app/components/widgets/designer-tool/designer-tool.widget.html @@ -21,9 +21,6 @@ } - @if (use.includes('SCREEN_TYPE') && screen.type === ScreenType.Responsive){ - - } @if (use.includes('SCREEN_TYPE')){ @@ -39,6 +36,18 @@ } + @if (use.includes('SCREEN_TYPE') && screen.type === ScreenType.Responsive){ + + + + @if(screen.width !== '100%' || screen.height !== '100%'){ + + } + } + + @if (use.includes('SCREEN_TYPE') && screen.type === ScreenType.Mobile){