Skip to content

Commit

Permalink
feat: allow dragging views to app instances running in different brow…
Browse files Browse the repository at this point in the history
…ser tabs or windows

closes: #168
  • Loading branch information
danielwiehl authored and mofogasy committed Aug 23, 2019
1 parent 14d76f0 commit 2ee9df3
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { async, fakeAsync, TestBed } from '@angular/core/testing';
import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.spec';
import { AbstractType, Component, InjectionToken, NgModule, Type } from '@angular/core';
import { ViewPartGridComponent } from '../view-part-grid/view-part-grid.component';
import { Router } from '@angular/router';
import { Router, UrlSegment } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { WorkbenchViewPartRegistry } from '../view-part-grid/workbench-view-part-registry.service';
import { WorkbenchRouter } from '../routing/workbench-router.service';
Expand Down Expand Up @@ -58,6 +58,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -102,6 +103,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -146,6 +148,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -190,6 +193,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -234,6 +238,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -276,6 +281,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.3',
viewUrlSegments: [new UrlSegment('view-3', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -291,6 +297,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -317,6 +324,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.1',
viewUrlSegments: [new UrlSegment('view-1', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -356,6 +364,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -380,6 +389,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.2',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -423,6 +433,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -447,6 +458,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.2',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -490,6 +502,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -514,6 +527,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.2',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -557,6 +571,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand All @@ -581,6 +596,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.2',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -632,6 +648,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.3',
viewUrlSegments: [new UrlSegment('view-3', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -660,6 +677,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.1',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -691,6 +709,7 @@ describe('ViewPartGridComponent', () => {
appInstanceId: workbench.appInstanceId,
viewPartRef: 'viewpart.3',
viewRef: 'view.2',
viewUrlSegments: [new UrlSegment('view-2', {})],
},
target: {
appInstanceId: workbench.appInstanceId,
Expand Down Expand Up @@ -803,3 +822,4 @@ class AppTestModule {
function getService<T>(token: Type<T> | AbstractType<T> | InjectionToken<T>): T {
return TestBed.get(token as Type<T>);
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { coerceArray } from '@angular/cdk/coercion';
import { filter, take, takeUntil } from 'rxjs/operators';
import { BroadcastChannelService } from '../broadcast-channel.service';
import { Defined } from '../defined.util';
import { UrlSegment } from '@angular/router';

/**
* Events fired during view drag and drop operation.
Expand Down Expand Up @@ -211,6 +212,7 @@ export interface ViewDragData {
viewTabPointerOffsetY: number;
viewRef: string;
viewTitle: string;
viewUrlSegments: UrlSegment[];
viewHeading: string;
viewClosable: boolean;
viewDirty: boolean;
Expand All @@ -227,6 +229,7 @@ export interface ViewMoveEvent {
source: {
viewRef: string;
viewPartRef: string;
viewUrlSegments: UrlSegment[],
appInstanceId: string;
};
target: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { asapScheduler, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { createElement, ElementCreateOptions, setStyle } from '../dom.util';
import { ViewDragData, ViewDragService } from './view-drag.service';
Expand All @@ -22,6 +22,9 @@ const NULL_BOUNDS: Bounds = null;
/**
* Adds a view drop zone to the host element allowing the view to be dropped either in the north,
* east, south, west or in the center.
*
* The drop zone is aligned with the target's bounds, thus requires the element to define a positioning context.
* If not positioned, the element is changed to be positioned relative.
*/
@Directive({
selector: '[wbViewDropZone]',
Expand All @@ -37,6 +40,12 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy {

private _dropRegion: Region;

/**
* Specifies which drop regions to allow. If not specified, all regions are allowed.
*/
@Input()
public wbViewDropZoneRegions: Region[];

/**
* Emits upon a view drop action.
*/
Expand All @@ -45,6 +54,9 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy {

constructor(host: ElementRef<HTMLElement>, private _viewDragService: ViewDragService, private _zone: NgZone) {
this._host = host.nativeElement;

// Ensure the host element to define a positioning context (after element creation)
asapScheduler.schedule(() => ensureHostElementPositioned(this._host));
}

public ngOnInit(): void {
Expand All @@ -54,9 +66,14 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy {

private onDragOver(event: DragEvent): void {
NgZone.assertNotInAngularZone();
event.preventDefault(); // allow view drop

const dropRegion = this.computeDropRegion(event);
if (dropRegion === undefined) {
this.renderDropRegions(NULL_BOUNDS, NULL_BOUNDS);
return;
}

event.preventDefault(); // allow view drop
if (dropRegion === this._dropRegion) {
return; // drop region did not change
}
Expand Down Expand Up @@ -234,32 +251,46 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy {
}
}

private computeDropRegion(event: DragEvent): Region {
private computeDropRegion(event: DragEvent): Region | undefined {
const horizontalDropZoneWidth = Math.min(DROP_REGION_MAX_SIZE, this._host.clientWidth / 3);
const verticalDropZoneHeight = Math.min(DROP_REGION_MAX_SIZE, this._host.clientHeight / 3);
const offsetX = event.pageX - this._host.getBoundingClientRect().left;
const offsetY = event.pageY - this._host.getBoundingClientRect().top;

if (offsetX < horizontalDropZoneWidth) {
if (this.supportsRegion('west') && offsetX < horizontalDropZoneWidth) {
return 'west';
}
if (offsetX > this._host.clientWidth - horizontalDropZoneWidth) {
if (this.supportsRegion('east') && offsetX > this._host.clientWidth - horizontalDropZoneWidth) {
return 'east';
}
if (offsetY < verticalDropZoneHeight) {
if (this.supportsRegion('north') && offsetY < verticalDropZoneHeight) {
return 'north';
}
if (offsetY > this._host.clientHeight - verticalDropZoneHeight) {
if (this.supportsRegion('south') && offsetY > this._host.clientHeight - verticalDropZoneHeight) {
return 'south';
}
return 'center';
if (this.supportsRegion('center')) {
return 'center';
}

return undefined;
}

private supportsRegion(region: Region): boolean {
return !this.wbViewDropZoneRegions || this.wbViewDropZoneRegions.includes(region);
}

public ngOnDestroy(): void {
this._destroy$.next();
}
}

function ensureHostElementPositioned(element: HTMLElement): void {
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
}

interface Bounds {
top: string;
right: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ViewTabContentComponent } from '../view-part/view-tab-content/view-tab-
import { WorkbenchView } from '../workbench.model';
import { WorkbenchConfig } from '../workbench.config';
import { VIEW_TAB_CONTEXT } from '../workbench.constants';
import { UrlSegment } from '@angular/router';

export type ConstrainFn = (rect: ViewDragImageRect) => ViewDragImageRect;

Expand Down Expand Up @@ -191,13 +192,15 @@ class DragImageWorkbenchView implements WorkbenchView {
public readonly active = true;
public readonly blocked = false;
public readonly cssClasses = [];
public readonly urlSegments: UrlSegment[];

constructor(dragData: ViewDragData) {
this.viewRef = dragData.viewRef;
this.title = dragData.viewTitle;
this.heading = dragData.viewHeading;
this.closable = dragData.viewClosable;
this.dirty = dragData.viewDirty;
this.urlSegments = dragData.viewUrlSegments;
}

public close(): Promise<boolean> {
Expand Down
Loading

0 comments on commit 2ee9df3

Please sign in to comment.