diff --git a/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.scss b/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.scss index 3eab63e92..8c5ecc1ab 100644 --- a/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.scss +++ b/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.scss @@ -1,45 +1,91 @@ +@use "../../../../../global/theme/mixins/button"; + app-slides-aside { - display: flex; - flex-direction: column; - min-height: 100%; - height: 100%; + position: relative; + + aside { + display: flex; + flex-direction: column; + + min-height: 100%; + height: 100%; - width: var(--slides-aside-width); + width: var(--slides-aside-width); - padding: 16px; - overflow: scroll; - border-right: 1px solid #dedede; + padding: 16px 16px 48px; + overflow: scroll; + border-right: 1px solid #dedede; - --preview-width: calc(var(--slides-aside-width) - 32px); + --preview-width: calc(var(--slides-aside-width) - 32px); + + &.drag { + app-slide-thumbnail { + transition: margin 0.25s ease-out, min-height 0.25s ease-in; + } + } - &.drag { app-slide-thumbnail { - transition: margin 0.25s ease-out, min-height 0.25s ease-in; + margin-bottom: 16px; + + transition: margin 0.15s ease-in; + + &.highlight { + border: 1px solid var(--ion-color-dark); + box-shadow: rgba(var(--ion-color-dark-rgb), 0.4) 0 1px 4px; + } + + &.hover { + margin-bottom: calc(var(--slides-aside-width) * 9 / 16); + } + + &.hover-top { + margin-top: calc(var(--slides-aside-width) * 9 / 16); + } + + &.drag-start, &.drag-hover { + visibility: hidden; + opacity: 0; + } + + &.drag-hover { + min-height: 0; + height: 0; + border: none; + } } } - app-slide-thumbnail { - margin-bottom: 16px; + div.actions { + position: absolute; + bottom: 0; + left: 0; - transition: margin 0.15s ease-in; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; - &.hover { - margin-bottom: calc((var(--slides-aside-width) - 32px) * 9 / 16); - } + width: 100%; - &.hover-top { - margin-top: calc((var(--slides-aside-width) - 32px) * 9 / 16); - } + background: var(--ion-color-light); + border: 1px solid #dedede; - &.drag-start, &.drag-hover { - visibility: hidden; - opacity: 0; - } + padding: 8px 16px; + } + + app-action-add-slide { + button { + --button-action-flex-direction: row; + + @include button.action; + + ion-icon { + @include button.icon; - &.drag-hover { - min-height: 0; - border: none; + font-size: 14px; + margin: 0 4px 0 0; + } } } } diff --git a/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.tsx b/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.tsx index ab948f143..46980d66a 100644 --- a/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.tsx +++ b/studio/src/app/components/editor/actions/app-slides-aside/app-slides-aside.tsx @@ -15,6 +15,9 @@ export class AppSlidesAside { @State() private slides: HTMLElement[] = []; + @Prop() + activeIndex: number; + @Prop() deckRef!: HTMLDeckgoDeckElement; @@ -28,6 +31,9 @@ export class AppSlidesAside { private readonly debounceUpdateSlide: (updateSlide: HTMLElement) => void; + private canDragLeave: boolean = true; + private canDragHover: boolean = true; + constructor() { this.debounceUpdateAllSlides = debounce(async () => { await this.updateAllSlides(); @@ -42,6 +48,13 @@ export class AppSlidesAside { this.debounceUpdateAllSlides(); } + componentDidUpdate() { + setTimeout(() => { + this.canDragLeave = true; + this.canDragHover = true; + }, 250); + } + @Listen('deckDidLoad', {target: 'document'}) onDeckDidLoad() { this.debounceUpdateAllSlides(); @@ -57,12 +70,27 @@ export class AppSlidesAside { this.debounceUpdateSlide(updatedSlide); } + @Listen('slideDelete', {target: 'document'}) + async onSlideDelete({detail: deletedSlide}: CustomEvent) { + await this.deleteSlide(deletedSlide); + } + private async updateSlide(updatedSlide: HTMLElement) { - const slideIndex: number = Array.from(updatedSlide.parentNode.children).indexOf(updatedSlide); + const slideIndex: number = this.slideIndex(updatedSlide); this.slides = [...this.slides.map((slide: HTMLElement, index: number) => (slideIndex === index ? (updatedSlide.cloneNode(true) as HTMLElement) : slide))]; } + private async deleteSlide(deletedSlide: HTMLElement) { + const slideIndex: number = this.slideIndex(deletedSlide); + + this.slides = [...this.slides.filter((_slide: HTMLElement, index: number) => slideIndex !== index)]; + } + + private slideIndex(slide: HTMLElement): number { + return Array.from(slide.parentNode.children).indexOf(slide); + } + private async updateAllSlides() { const slides: NodeListOf = document.querySelectorAll(`${deckSelector} > *`); @@ -84,10 +112,18 @@ export class AppSlidesAside { } private onDragHover(to: number) { - if (!this.reorderDetail) { + if (!this.canDragHover) { return; } + if (!this.reorderDetail || this.reorderDetail.to === to) { + return; + } + + if (this.reorderDetail.to === -1 && to === 0) { + this.canDragLeave = false; + } + this.reorderDetail = { ...this.reorderDetail, to @@ -95,6 +131,10 @@ export class AppSlidesAside { } private onDragLeave() { + if (!this.canDragLeave) { + return; + } + if (!this.reorderDetail) { return; } @@ -103,6 +143,8 @@ export class AppSlidesAside { return; } + this.canDragHover = false; + this.reorderDetail = { ...this.reorderDetail, to: -1 @@ -131,34 +173,57 @@ export class AppSlidesAside { render() { return ( - + {this.renderSlides()} + + {this.renderActions()} + + ); + } + + private renderSlides() { + return ( + + ); + } + + private renderThumbnail(slide: HTMLElement, index: number) { + const dragClass: string = + index === this.reorderDetail?.to && this.reorderDetail?.from !== this.reorderDetail?.to + ? 'hover' + : index === 0 && this.reorderDetail?.to === -1 + ? 'hover-top' + : index === this.reorderDetail?.from + ? index === this.reorderDetail?.to + ? 'drag-start' + : 'drag-hover' + : ''; + + return ( + await slideTo(index)} + key={slide.getAttribute('slide_id')} + slide={slide} + deck={this.deckRef} + class={`${dragClass} ${this.activeIndex === index ? 'highlight' : ''}`} + draggable={true} + onDragStart={() => this.onDragStart(index)} + onDragOver={() => this.onDragHover(index)}> + ); + } + + private renderActions() { + return ( +
+ +
); } } diff --git a/studio/src/app/components/editor/actions/deck/app-action-add-slide/app-action-add-slide.tsx b/studio/src/app/components/editor/actions/deck/app-action-add-slide/app-action-add-slide.tsx index 836b445fe..3ea229673 100644 --- a/studio/src/app/components/editor/actions/deck/app-action-add-slide/app-action-add-slide.tsx +++ b/studio/src/app/components/editor/actions/deck/app-action-add-slide/app-action-add-slide.tsx @@ -1,4 +1,4 @@ -import {Component, Element, EventEmitter, h, JSX, Prop} from '@stencil/core'; +import {Component, Element, Event, EventEmitter, h, JSX, Prop} from '@stencil/core'; import {modalController, OverlayEventDetail, popoverController} from '@ionic/core'; @@ -20,16 +20,19 @@ export class AppActionAddSlide { @Element() el: HTMLElement; @Prop() - slides: JSX.IntrinsicElements[] = []; + slidesLength: number | undefined; @Prop() - blockSlide: EventEmitter; + popoverCssClass: string; - @Prop() - signIn: EventEmitter; + @Event({bubbles: true}) + private signIn: EventEmitter; - @Prop() - addSlide: EventEmitter; + @Event({bubbles: true}) + private addSlide: EventEmitter; + + @Event({bubbles: true}) + private blockSlide: EventEmitter; private anonymousService: AnonymousService; @@ -42,7 +45,7 @@ export class AppActionAddSlide { return; } - const couldAddSlide: boolean = await this.anonymousService.couldAddSlide(this.slides); + const couldAddSlide: boolean = await this.anonymousService.couldAddSlide(this.slidesLength); if (!couldAddSlide) { this.signIn.emit(); @@ -53,7 +56,7 @@ export class AppActionAddSlide { component: 'app-create-slide', mode: 'ios', showBackdrop: false, - cssClass: 'popover-menu popover-menu-wide' + cssClass: `popover-menu popover-menu-wide ${this.popoverCssClass}` }); popover.onDidDismiss().then(async (detail: OverlayEventDetail) => { @@ -276,10 +279,7 @@ export class AppActionAddSlide { render() { return ( - this.onActionOpenSlideAdd($event)}> + this.onActionOpenSlideAdd($event)}> ); diff --git a/studio/src/app/components/editor/actions/deck/app-actions-deck/app-actions-deck.tsx b/studio/src/app/components/editor/actions/deck/app-actions-deck/app-actions-deck.tsx index 27ef6a462..8887e680a 100644 --- a/studio/src/app/components/editor/actions/deck/app-actions-deck/app-actions-deck.tsx +++ b/studio/src/app/components/editor/actions/deck/app-actions-deck/app-actions-deck.tsx @@ -1,6 +1,7 @@ import {Component, Element, Event, EventEmitter, h, JSX, Prop} from '@stencil/core'; import {modalController, OverlayEventDetail, popoverController} from '@ionic/core'; +import {isMobile} from '@deckdeckgo/utils'; import {ConnectionState, DeckdeckgoEventDeckRequest} from '@deckdeckgo/types'; import offlineStore from '../../../../../stores/offline.store'; @@ -30,15 +31,6 @@ export class AppActionsDeck { @Prop() slides: JSX.IntrinsicElements[] = []; - @Prop() - blockSlide: EventEmitter; - - @Prop() - signIn: EventEmitter; - - @Prop() - addSlide: EventEmitter; - @Prop() animatePrevNextSlide: EventEmitter; @@ -56,6 +48,9 @@ export class AppActionsDeck { private destroyListener; + // Drag and drop is not supported on iOS and Firefox on Android + private mobile: boolean = isMobile(); + async componentWillLoad() { this.destroyListener = remoteStore.onChange('pendingRequests', async (requests: DeckdeckgoEventDeckRequest[] | undefined) => { if (requests && requests.length > 0) { @@ -160,8 +155,6 @@ export class AppActionsDeck { const popover: HTMLIonPopoverElement = await popoverController.create({ component: 'app-deck-style', componentProps: { - signIn: this.signIn, - blockSlide: this.blockSlide, deckDidChange: this.deckDidChange }, mode: 'ios', @@ -240,7 +233,7 @@ export class AppActionsDeck { return (