improved uploading of files (#1730)

* improved uploading of files

- new core/UploadDirective to allow dropping files to any html element
- enhanced file dropping for DataTable rows (disabled by default)
- enhanced file dropping for DocumentList rows (disabled by default)
- upload drop area now handles file uploads for child elements (i.e.
rows in the document list)

* fix unit tests

* unit tests and code cleanup

* #1732, fix upload of folders
This commit is contained in:
Denys Vuika
2017-03-16 16:28:18 +00:00
committed by Mario Romano
parent f3de023ab3
commit 3fee3b5002
19 changed files with 497 additions and 97 deletions

View File

@@ -18,6 +18,7 @@
[currentFolderId]="currentFolderId" [currentFolderId]="currentFolderId"
[contextMenuActions]="true" [contextMenuActions]="true"
[contentActions]="true" [contentActions]="true"
[allowDropFiles]="true"
(error)="onNavigationError($event)" (error)="onNavigationError($event)"
(success)="resetError()" (success)="resetError()"
(preview)="showFile($event)"> (preview)="showFile($event)">

View File

@@ -43,26 +43,134 @@ necessary configuration, see this [page](https://github.com/Alfresco/alfresco-ng
npm install --save ng2-alfresco-core npm install --save ng2-alfresco-core
``` ```
## Main components and services ## Library content
### Components - Components
- Context Menu directive
- Material Design directives
- [mdl]
- [alfresco-mdl-button]
- [alfresco-mdl-menu]
- [alfresco-mdl-tabs]
- Directives
- UploadDirective
- Services
- **LogService**, log service implementation
- **NotificationService**, Notification service implementation
- **AlfrescoApiService**, provides access to Alfresco JS API instance
- **AlfrescoAuthenticationService**, main authentication APIs
- **AlfrescoTranslationService**, various i18n-related APIs
- **ContextMenuService**, global context menu APIs
- Context Menu directive
- Material Design directives
- [mdl]
- [alfresco-mdl-button]
- [alfresco-mdl-menu]
- [alfresco-mdl-tabs]
### Services ## UploadDirective
- **LogService**, log service implementation Allows your components or common HTML elements reacting on File drag and drop in order to upload content.
- **NotificationService**, Notification service implementation Used by attaching to an element or component.
- **AlfrescoApiService**, provides access to Alfresco JS API instance
- **AlfrescoAuthenticationService**, main authentication APIs
- **AlfrescoTranslationService**, various i18n-related APIs
- **ContextMenuService**, global context menu APIs
### Basic usage
The directive itself does not do any file management process,
but collects information on dropped files and raises corresponding events instead.
```html
<div style="width:100px; height:100px"
[adf-upload]="true"
[adf-upload-data]="{ some: 'data' }">
Drop files here...
</div>
```
It is possible controlling when upload behaviour is enabled/disabled by binding directive to a `boolean` value or expression:
```html
<div [adf-upload]="true">...</div>
<div [adf-upload]="allowUpload">...</div>
<div [adf-upload]="isUploadEnabled()">...</div>
```
### Events
Once a single or multiple files are dropped on the decorated element the `upload-files` [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) is raised.
The DOM event is configured to have `bubbling` enabled, so any component up the component tree can handle, process or prevent it:
```html
<div (upload-files)="onUploadFiles($event)">
<div [adf-upload]="true"></div>
</div>
```
```ts
onUploadFiles(e: CustomEvent) {
console.log(e.detail.files);
// your code
}
```
Please note that event will be raised only if valid [Files](https://developer.mozilla.org/en-US/docs/Web/API/File) were dropped onto the decorated element.
The `upload-files` event is cancellable, so you can stop propagation of the drop event to uppper levels in case it has been already handled by your code:
```ts
onUploadFiles(e: CustomEvent) {
e.stopPropagation();
e.preventDefault();
// your code
}
```
It is also possible attaching arbitrary data to each event in order to access it from within external event handlers.
A typical scenario is data tables where you may want to handle also the data row and/or underlying data to be accessible upon files drop.
You may be using `adf-upload-data` to bind custom values or objects for every event raised:
```html
<div [adf-upload]="true" [adf-upload-data]="dataRow"></div>
<div [adf-upload]="true" [adf-upload-data]="'string value'"></div>
<div [adf-upload]="true" [adf-upload-data]="{ name: 'custom object' }"></div>
<div [adf-upload]="true" [adf-upload-data]="getUploadData()"></div>
```
As part of the `details` property of the [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) you can get access to the following:
```ts
detail: {
sender: UploadDirective, // directive that raised given event
data: any, // arbitrary data associated (bound)
files: File[] // dropped files
}
```
### Styling
The decorated element gets `adf-upload__dragging` CSS class name in the class list every time files are dragged over it.
This allows changing look and feel of your components in case additional visual indication is required,
for example you may want drawing a dashed border around the table row on drag:
```html
<table>
<tr [adf-upload]="true">
...
</tr>
</table>
```
```css
.adf-upload__dragging > td:first-child {
border-left: 1px dashed rgb(68,138,255);
}
.adf-upload__dragging > td {
border-top: 1px dashed rgb(68,138,255);
border-bottom: 1px dashed rgb(68,138,255);
}
.adf-upload__dragging > td:last-child {
border-right: 1px dashed rgb(68,138,255);
}
```
## Alfresco Api Service ## Alfresco Api Service
@@ -78,7 +186,7 @@ export class MyComponent implements OnInit {
ngOnInit() { ngOnInit() {
let nodeId = 'some-node-id'; let nodeId = 'some-node-id';
let params = {}; let params = {};
this.getAlfrescoApi().nodes this.apiService.getInstance().nodes
.getNodeChildren(nodeId, params) .getNodeChildren(nodeId, params)
.then(result => console.log(result)); .then(result => console.log(result));
} }
@@ -92,8 +200,8 @@ In case of any TypeScript type check errors you can still call any supported
Alfresco JS api by casting the instance to `any` type like the following:_ Alfresco JS api by casting the instance to `any` type like the following:_
```ts ```ts
let apiService: any = this.authService.getAlfrescoApi(); let api: any = this.apiService.getInstance();
apiService.nodes.addNode('-root-', body, {}); api.nodes.addNode('-root-', body, {});
``` ```
## Notification Service ## Notification Service

View File

@@ -39,6 +39,7 @@ import {
NotificationService NotificationService
} from './src/services/index'; } from './src/services/index';
import { UploadDirective } from './src/directives/upload.directive';
import { DataColumnComponent } from './src/components/data-column/data-column.component'; import { DataColumnComponent } from './src/components/data-column/data-column.component';
import { DataColumnListComponent } from './src/components/data-column/data-column-list.component'; import { DataColumnListComponent } from './src/components/data-column/data-column-list.component';
import { MATERIAL_DESIGN_DIRECTIVES } from './src/components/material/index'; import { MATERIAL_DESIGN_DIRECTIVES } from './src/components/material/index';
@@ -48,6 +49,7 @@ export * from './src/services/index';
export * from './src/components/index'; export * from './src/components/index';
export * from './src/components/data-column/data-column.component'; export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component'; export * from './src/components/data-column/data-column-list.component';
export * from './src/directives/upload.directive';
export * from './src/utils/index'; export * from './src/utils/index';
export * from './src/events/base.event'; export * from './src/events/base.event';
export * from './src/events/base-ui.event'; export * from './src/events/base-ui.event';
@@ -90,6 +92,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
declarations: [ declarations: [
...MATERIAL_DESIGN_DIRECTIVES, ...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES, ...CONTEXT_MENU_DIRECTIVES,
UploadDirective,
DataColumnComponent, DataColumnComponent,
DataColumnListComponent DataColumnListComponent
], ],
@@ -105,6 +108,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
TranslateModule, TranslateModule,
...MATERIAL_DESIGN_DIRECTIVES, ...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES, ...CONTEXT_MENU_DIRECTIVES,
UploadDirective,
DataColumnComponent, DataColumnComponent,
DataColumnListComponent DataColumnListComponent
] ]

View File

@@ -0,0 +1,136 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ElementRef } from '@angular/core';
import { UploadDirective } from './upload.directive';
describe('UploadDirective', () => {
let directive: UploadDirective;
let nativeElement: any;
beforeEach(() => {
nativeElement = {
dispatchEvent: () => {}
};
directive = new UploadDirective(new ElementRef(nativeElement));
});
it('should be enabled by default', () => {
expect(directive.enabled).toBeTruthy();
});
it('should have debug mode switched off by default', () => {
expect(directive.debug).toBeFalsy();
});
it('should update drag status on dragenter', () => {
expect(directive.isDragging).toBeFalsy();
directive.enabled = true;
directive.onDragEnter();
expect(directive.isDragging).toBeTruthy();
});
it('should not update drag status on dragenter when disabled', () => {
expect(directive.isDragging).toBeFalsy();
directive.enabled = false;
directive.onDragEnter();
expect(directive.isDragging).toBeFalsy();
});
it('should update drag status on dragover', () => {
expect(directive.isDragging).toBeFalsy();
directive.enabled = true;
directive.onDragOver(null);
expect(directive.isDragging).toBeTruthy();
});
it('should prevent default event on dragover', () => {
let event = new Event('dom-event');
spyOn(event, 'preventDefault').and.stub();
directive.enabled = true;
directive.onDragOver(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(directive.isDragging).toBeTruthy();
});
it('should not update drag status on dragover when disabled', () => {
expect(directive.isDragging).toBeFalsy();
directive.enabled = false;
directive.onDragOver(null);
});
it('should update drag status on dragleave', () => {
directive.enabled = true;
directive.isDragging = true;
directive.onDragLeave();
expect(directive.isDragging).toBeFalsy();
});
it('should not update drag status on dragleave when disabled', () => {
directive.enabled = false;
directive.isDragging = true;
directive.onDragLeave();
expect(directive.isDragging).toBeTruthy();
});
it('should prevent default event on drop', () => {
directive.enabled = true;
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
directive.onDrop(<DragEvent>event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should stop default event propagation on drop', () => {
directive.enabled = true;
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
directive.onDrop(<DragEvent>event);
expect(event.stopPropagation).toHaveBeenCalled();
});
it('should not prevent default event on drop when disabled', () => {
directive.enabled = false;
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
directive.onDrop(<DragEvent>event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('should raise upload-files event on files drop', () => {
directive.enabled = true;
let files = [<File> {}];
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
spyOn(directive, 'getFilesDropped').and.returnValue(files);
spyOn(nativeElement, 'dispatchEvent').and.stub();
directive.onDrop(event);
expect(nativeElement.dispatchEvent).toHaveBeenCalled();
});
it('should provide dropped files in upload-files event', () => {
directive.enabled = true;
let files = [<File> {}];
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
spyOn(directive, 'getFilesDropped').and.returnValue(files);
spyOn(nativeElement, 'dispatchEvent').and.callFake(e => {
expect(e.detail.files.length).toBe(1);
expect(e.detail.files[0]).toBe(files[0]);
});
directive.onDrop(event);
expect(nativeElement.dispatchEvent).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,113 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, Input, HostBinding, HostListener, ElementRef } from '@angular/core';
@Directive({
selector: '[adf-upload]'
})
export class UploadDirective {
@Input('adf-upload')
enabled: boolean = true;
@Input('adf-upload-data')
data: any;
@Input()
debug: boolean = false;
@HostBinding('class.adf-upload__dragging')
isDragging: boolean;
constructor(private el: ElementRef) {
}
@HostListener('dragenter')
onDragEnter() {
if (this.enabled) {
this.isDragging = true;
}
}
@HostListener('dragover', ['$event'])
onDragOver(event: Event) {
if (this.enabled) {
if (event) {
event.preventDefault();
}
this.isDragging = true;
}
}
@HostListener('dragleave')
onDragLeave() {
if (this.enabled) {
this.isDragging = false;
}
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
if (this.enabled) {
event.preventDefault();
event.stopPropagation();
this.isDragging = false;
let files = this.getFilesDropped(event.dataTransfer);
if (files.length > 0) {
let e = new CustomEvent('upload-files', {
detail: {
sender: this,
data: this.data,
files: files
},
bubbles: true
});
this.el.nativeElement.dispatchEvent(e);
}
}
}
/**
* Extract files from the DataTransfer object used to hold the data that is being dragged during a drag and drop operation.
* @param dataTransfer DataTransfer object
*/
protected getFilesDropped(dataTransfer: DataTransfer): File[] {
let result: File[] = [];
if (dataTransfer) {
let items: DataTransferItemList = dataTransfer.items;
if (items && items.length > 0) {
for (let i = 0; i < items.length; i++) {
let item: DataTransferItem = items[i];
if (item.type) {
let file = item.getAsFile();
if (file) {
result.push(file);
}
}
}
}
}
return result;
}
}

View File

@@ -184,6 +184,7 @@ You can also use HTML-based schema declaration like shown below:
| `actionsPosition` | string (left\|right) | right | Position of the actions dropdown menu. | | `actionsPosition` | string (left\|right) | right | Position of the actions dropdown menu. |
| `fallbackThumbnail` | string | | Fallback image for row ehre thubnail is missing| | `fallbackThumbnail` | string | | Fallback image for row ehre thubnail is missing|
| `contextMenu` | boolean | false | Toggles custom context menu for the component | | `contextMenu` | boolean | false | Toggles custom context menu for the component |
| `allowDropFiles` | boolean | false | Toggle file drop support for rows (see **ng2-alfresco-core/UploadDirective** for more details) |
### DataColumn Properties ### DataColumn Properties

View File

@@ -122,3 +122,16 @@
.alfresco-datatable__row--selected { .alfresco-datatable__row--selected {
color: rgb(68,138,255); color: rgb(68,138,255);
} }
.adf-upload__dragging > td {
border-top: 1px dashed rgb(68,138,255);
border-bottom: 1px dashed rgb(68,138,255);
}
.adf-upload__dragging > td:first-child {
border-left: 1px dashed rgb(68,138,255);
}
.adf-upload__dragging > td:last-child {
border-right: 1px dashed rgb(68,138,255);
}

View File

@@ -37,7 +37,8 @@
<tr *ngFor="let row of data.getRows(); let idx = index" tabindex="0" <tr *ngFor="let row of data.getRows(); let idx = index" tabindex="0"
class="alfresco-datatable__row" class="alfresco-datatable__row"
[class.alfresco-datatable__row--selected]="selectedRow === row"> [class.alfresco-datatable__row--selected]="selectedRow === row"
[adf-upload]="allowDropFiles" [adf-upload-data]="row">
<!-- Actions (right) --> <!-- Actions (right) -->
<td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell"> <td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell">

View File

@@ -51,6 +51,9 @@ export class DataTableComponent implements AfterContentInit {
@Input() @Input()
contextMenu: boolean = false; contextMenu: boolean = false;
@Input()
allowDropFiles: boolean = false;
@Output() @Output()
rowClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>(); rowClick: EventEmitter<DataRowEvent> = new EventEmitter<DataRowEvent>();

View File

@@ -184,9 +184,10 @@ The properties currentFolderId, folderNode and node are the entry initialization
| `contentActionsPosition` | string (left\|right) | right | Position of the content actions dropdown menu. | | `contentActionsPosition` | string (left\|right) | right | Position of the content actions dropdown menu. |
| `contextMenuActions` | boolean | false | Toggles context menus for each row | | `contextMenuActions` | boolean | false | Toggles context menus for each row |
| `enablePagination` | boolean | true | Shows pagination | | `enablePagination` | boolean | true | Shows pagination |
| `creationMenuActions` | boolean | true | Toggles the creation menu actions| | `creationMenuActions` | boolean | true | Toggles the creation menu actions |
| `rowFilter` | `RowFilter` | | Custom row filter, [see more](#custom-row-filter). | `rowFilter` | `RowFilter` | | Custom row filter, [see more](#custom-row-filter). |
| `imageResolver` | `ImageResolver` | | Custom image resolver, [see more](#custom-image-resolver). | `imageResolver` | `ImageResolver` | | Custom image resolver, [see more](#custom-image-resolver). |
| `allowDropFiles` | boolean | false | Toggle file drop support for rows (see **ng2-alfresco-core/UploadDirective** for more details) |
### Events ### Events

View File

@@ -10,6 +10,7 @@
[actionsPosition]="contentActionsPosition" [actionsPosition]="contentActionsPosition"
[multiselect]="multiselect" [multiselect]="multiselect"
[fallbackThumbnail]="fallbackThumbnail" [fallbackThumbnail]="fallbackThumbnail"
[allowDropFiles]="allowDropFiles"
[contextMenu]="contextMenuActions" [contextMenu]="contextMenuActions"
(showRowContextMenu)="onShowRowContextMenu($event)" (showRowContextMenu)="onShowRowContextMenu($event)"
(showRowActionsMenu)="onShowRowActionsMenu($event)" (showRowActionsMenu)="onShowRowActionsMenu($event)"

View File

@@ -82,6 +82,9 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Input() @Input()
emptyFolderImageUrl: string = this.baseComponentPath + 'assets/images/empty_doc_lib.svg'; emptyFolderImageUrl: string = this.baseComponentPath + 'assets/images/empty_doc_lib.svg';
@Input()
allowDropFiles: boolean = false;
skipCount: number = 0; skipCount: number = 0;
pagination: Pagination; pagination: Pagination;

View File

@@ -128,7 +128,7 @@ export class UploadButtonComponent {
let directoryName = this.getDirectoryName(directoryPath); let directoryName = this.getDirectoryName(directoryPath);
let absolutePath = this.currentFolderPath + this.getDirectoryPath(directoryPath); let absolutePath = this.currentFolderPath + this.getDirectoryPath(directoryPath);
this.uploadService.createFolder(absolutePath, directoryName) this.uploadService.createFolder(absolutePath, directoryName, this.rootFolderId)
.subscribe( .subscribe(
res => { res => {
let relativeDir = this.currentFolderPath + '/' + directoryPath; let relativeDir = this.currentFolderPath + '/' + directoryPath;

View File

@@ -4,8 +4,7 @@
text-align: center; text-align: center;
} }
.input-focus { .file-draggable__input-focus {
color: #2196F3; color: #2196F3;
margin-left: 3px; border: 1px dashed #2196F3;
border: 3px dashed #2196F3;
} }

View File

@@ -2,6 +2,7 @@
(onFilesDropped)="onFilesDropped($event)" (onFilesDropped)="onFilesDropped($event)"
(onFilesEntityDropped)="onFilesEntityDropped($event)" (onFilesEntityDropped)="onFilesEntityDropped($event)"
(onFolderEntityDropped)="onFolderEntityDropped($event)" (onFolderEntityDropped)="onFolderEntityDropped($event)"
(upload-files)="onUploadFiles($event)"
dropzone="" webkitdropzone="*" #droparea> dropzone="" webkitdropzone="*" #droparea>
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@@ -72,16 +72,35 @@ export class UploadDragAreaComponent {
this.uploadService.setOptions(formFields, this.versioning); this.uploadService.setOptions(formFields, this.versioning);
} }
/**
* Handles 'upload-files' events raised by child components.
* @param e DOM event
*/
onUploadFiles(e: CustomEvent) {
e.stopPropagation();
e.preventDefault();
let files = e.detail.files;
if (files && files.length > 0) {
if (e.detail.data.obj.entry.isFolder) {
let id = e.detail.data.obj.entry.id;
this.onFilesDropped(files, id, '/');
} else {
this.onFilesDropped(files);
}
}
}
/** /**
* Method called when files are dropped in the drag area. * Method called when files are dropped in the drag area.
* *
* @param {File[]} files - files dropped in the drag area. * @param {File[]} files - files dropped in the drag area.
*/ */
onFilesDropped(files: File[]): void { onFilesDropped(files: File[], rootId?: string, directory?: string): void {
if (files.length) { if (files.length) {
if (this.checkValidity(files)) { if (this.checkValidity(files)) {
this.uploadService.addToQueue(files); this.uploadService.addToQueue(files);
this.uploadService.uploadFilesInTheQueue(this.rootFolderId, this.currentFolderPath, this.onSuccess); this.uploadService.uploadFilesInTheQueue(rootId || this.rootFolderId, directory || this.currentFolderPath, this.onSuccess);
let latestFilesAdded = this.uploadService.getQueue(); let latestFilesAdded = this.uploadService.getQueue();
if (this.showNotificationBar) { if (this.showNotificationBar) {
this.showUndoNotificationBar(latestFilesAdded); this.showUndoNotificationBar(latestFilesAdded);
@@ -132,7 +151,7 @@ export class UploadDragAreaComponent {
let relativePath = folder.fullPath.replace(folder.name, ''); let relativePath = folder.fullPath.replace(folder.name, '');
relativePath = this.currentFolderPath + relativePath; relativePath = this.currentFolderPath + relativePath;
this.uploadService.createFolder(relativePath, folder.name) this.uploadService.createFolder(relativePath, folder.name, this.rootFolderId)
.subscribe( .subscribe(
message => { message => {
this.onSuccess.emit({ this.onSuccess.emit({

View File

@@ -19,7 +19,7 @@ import { FileDraggableDirective } from '../directives/file-draggable.directive';
describe('FileDraggableDirective', () => { describe('FileDraggableDirective', () => {
let component; let component: FileDraggableDirective;
beforeEach( () => { beforeEach( () => {
component = new FileDraggableDirective(); component = new FileDraggableDirective();
@@ -51,7 +51,7 @@ describe('FileDraggableDirective', () => {
done(); done();
}); });
component._onDropFiles(fakeEvent); component.onDropFiles(fakeEvent);
}); });
it('should emit onFilesDropped event when a file is dragged not with Chrome' , (done) => { it('should emit onFilesDropped event when a file is dragged not with Chrome' , (done) => {
@@ -70,7 +70,7 @@ describe('FileDraggableDirective', () => {
done(); done();
}); });
component._onDropFiles(fakeEvent); component.onDropFiles(fakeEvent);
}); });
it('should emit onFilesDropped event when a file is dragged with Chrome', (done) => { it('should emit onFilesDropped event when a file is dragged with Chrome', (done) => {
@@ -90,7 +90,7 @@ describe('FileDraggableDirective', () => {
done(); done();
}); });
component._onDropFiles(fakeEvent); component.onDropFiles(fakeEvent);
}); });
it('should take the focus when the drag enter is called', () => { it('should take the focus when the drag enter is called', () => {
@@ -98,7 +98,7 @@ describe('FileDraggableDirective', () => {
spyOn(mockEvent, 'preventDefault'); spyOn(mockEvent, 'preventDefault');
expect(component.getInputFocus()).toBe(false); expect(component.getInputFocus()).toBe(false);
component._onDragEnter(mockEvent); component.onDragEnter(mockEvent);
expect(component.getInputFocus()).toBe(true); expect(component.getInputFocus()).toBe(true);
}); });
}); });

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Directive, EventEmitter, Output } from '@angular/core'; import { Directive, HostListener, HostBinding, EventEmitter, Output } from '@angular/core';
/** /**
* [file-draggable] * [file-draggable]
@@ -29,17 +29,12 @@ import { Directive, EventEmitter, Output } from '@angular/core';
* @returns {FileDraggableDirective} . * @returns {FileDraggableDirective} .
*/ */
@Directive({ @Directive({
selector: '[file-draggable]', selector: '[file-draggable]'
host: {
'(drop)': '_onDropFiles($event)',
'(dragenter)': '_onDragEnter($event)',
'(dragleave)': '_onDragLeave($event)',
'(dragover)': '_onDragOver($event)',
'[class.input-focus]': '_inputFocusClass'
}
}) })
export class FileDraggableDirective { export class FileDraggableDirective {
files: File [];
@Output() @Output()
onFilesDropped: EventEmitter<any> = new EventEmitter(); onFilesDropped: EventEmitter<any> = new EventEmitter();
@@ -49,41 +44,39 @@ export class FileDraggableDirective {
@Output() @Output()
onFolderEntityDropped: EventEmitter<any> = new EventEmitter(); onFolderEntityDropped: EventEmitter<any> = new EventEmitter();
files: File []; @HostBinding('class.file-draggable__input-focus')
inputFocusClass: boolean = false;
private _inputFocusClass: boolean = false;
constructor() {
}
/** /**
* Method called when files is dropped in the drag and drop area. * Method called when files is dropped in the drag and drop area.
* * @param event DOM event.
* @param {$event} $event - DOM $event.
*/ */
_onDropFiles($event: any): void { @HostListener('drop', ['$event'])
this._preventDefault($event); onDropFiles(event: any): void {
if (!event.defaultPrevented) {
this.preventDefault(event);
let items = $event.dataTransfer.items; let items = event.dataTransfer.items;
if (items) { if (items) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (typeof items[i].webkitGetAsEntry !== 'undefined') { if (typeof items[i].webkitGetAsEntry !== 'undefined') {
let item = items[i].webkitGetAsEntry(); let item = items[i].webkitGetAsEntry();
if (item) { if (item) {
this._traverseFileTree(item); this.traverseFileTree(item);
}
} else {
let files = event.dataTransfer.files;
this.onFilesDropped.emit(files);
} }
} else {
let files = $event.dataTransfer.files;
this.onFilesDropped.emit(files);
} }
} else {
// safari or FF
let files = event.dataTransfer.files;
this.onFilesDropped.emit(files);
} }
} else {
// safari or FF
let files = $event.dataTransfer.files;
this.onFilesDropped.emit(files);
}
this._inputFocusClass = false; this.inputFocusClass = false;
}
} }
/** /**
@@ -91,7 +84,7 @@ export class FileDraggableDirective {
* *
* @param {Object} item - can contains files or folders. * @param {Object} item - can contains files or folders.
*/ */
private _traverseFileTree(item: any): void { private traverseFileTree(item: any): void {
if (item.isFile) { if (item.isFile) {
let self = this; let self = this;
self.onFilesEntityDropped.emit(item); self.onFilesEntityDropped.emit(item);
@@ -105,44 +98,50 @@ export class FileDraggableDirective {
/** /**
* Change the style of the drag area when a file drag in. * Change the style of the drag area when a file drag in.
* *
* @param {$event} $event - DOM $event. * @param {event} event - DOM event.
*/ */
_onDragEnter($event: Event): void { @HostListener('dragenter', ['$event'])
this._preventDefault($event); onDragEnter(event: Event): void {
if (!event.defaultPrevented) {
this._inputFocusClass = true; this.preventDefault(event);
this.inputFocusClass = true;
}
} }
/** /**
* Change the style of the drag area when a file drag out. * Change the style of the drag area when a file drag out.
* *
* @param {$event} $event - DOM $event. * @param {event} event - DOM event.
*/ */
_onDragLeave($event: Event): void { @HostListener('dragleave', ['$event'])
this._preventDefault($event); onDragLeave(event: Event): void {
if (!event.defaultPrevented) {
this._inputFocusClass = false; this.preventDefault(event);
this.inputFocusClass = false;
}
} }
/** /**
* Change the style of the drag area when a file is over the drag area. * Change the style of the drag area when a file is over the drag area.
* *
* @param $event * @param event
* @private
*/ */
_onDragOver($event: Event): void { @HostListener('dragover', ['$event'])
this._preventDefault($event); onDragOver(event: Event): void {
this._inputFocusClass = true; if (!event.defaultPrevented) {
this.preventDefault(event);
this.inputFocusClass = true;
}
} }
/** /**
* Prevent default and stop propagation of the DOM event. * Prevent default and stop propagation of the DOM event.
* *
* @param {$event} $event - DOM $event. * @param {event} $event - DOM event.
*/ */
_preventDefault($event: Event): void { preventDefault(event: Event): void {
$event.stopPropagation(); event.stopPropagation();
$event.preventDefault(); event.preventDefault();
} }
/** /**
@@ -150,6 +149,6 @@ export class FileDraggableDirective {
* @returns {boolean} * @returns {boolean}
*/ */
getInputFocus () { getInputFocus () {
return this._inputFocusClass; return this.inputFocusClass;
} }
} }

View File

@@ -162,17 +162,14 @@ export class UploadService {
* Create a folder * Create a folder
* @param name - the folder name * @param name - the folder name
*/ */
createFolder(relativePath: string, name: string) { createFolder(relativePath: string, name: string, parentId?: string) {
return Observable.fromPromise(this.callApiCreateFolder(relativePath, name)) return Observable.fromPromise(this.callApiCreateFolder(relativePath, name, parentId))
.map(res => {
return res;
})
.do(data => this.logService.info('Node data', data)) // eyeball results in the console .do(data => this.logService.info('Node data', data)) // eyeball results in the console
.catch(err => this.handleError(err)); .catch(err => this.handleError(err));
} }
callApiCreateFolder(relativePath: string, name: string): Promise<MinimalNodeEntity> { callApiCreateFolder(relativePath: string, name: string, parentId?: string): Promise<MinimalNodeEntity> {
return this.apiService.getInstance().nodes.createFolder(name, relativePath); return this.apiService.getInstance().nodes.createFolder(name, relativePath, parentId);
} }
/** /**