[ADF-4272] TaskListCloud - improvements on CopyClipboardDirective (#4547)

* [ADF-4272] DocumentList -  add Copy content tooltip directive

* [ADF-4272] - fix build issue

* [ADF-4272] - change directive name and add requested changes

* [ADF-4272] - reset task-list-cloud html content

* [ADF-4272] - fix build

* [AFG-4272] - change name to CopyClipboard

* [ADF-4272] - PR changes

* [ADF-4272] - fix tests

* [ADF-4272[] - lint

* [ADF-4272] - merge clipboard directive with copy-content directive

* [ADF-4272] - PR changes

* [ADF-4272] - change docs
This commit is contained in:
Silviu Popa 2019-04-08 18:37:37 +03:00 committed by Eugenio Romano
parent dee63e3f3b
commit a87d1ef002
19 changed files with 433 additions and 74 deletions

View File

@ -179,7 +179,9 @@
"REPLACE_COLUMNS": "Replace columns",
"LOAD_NODE": "Load Node",
"MULTISELECT": "Multiselect",
"MULTISELECT_DESCRIPTION": "Use Cmd (Mac) or Ctrl (Windows) to toggle selection of multiple items"
"MULTISELECT_DESCRIPTION": "Use Cmd (Mac) or Ctrl (Windows) to toggle selection of multiple items",
"CLICK_TO_COPY": "Click to copy",
"SUCCESS_COPY": "Text copied to clipboard"
},
"ANALYTICS_REPORT": {
"NO_REPORT_MESSAGE": "No report selected. Choose a report from the list"

View File

@ -658,6 +658,27 @@ the total height of all rows exceeds the fixed height of the parent element.
</div>
```
### CopyClipboardDirective example
See the [Copy Content Directive ](../directives/clipboard.directive.md) page for full details of the directive
Json config file:
```json
[
{"type": "text", "key": "id", "title": "Id", "copyContent": "true"},
{"type": "text", "key": "name", "title": "name"},
]
```
HTML data-columns
```html
<adf-tasklist ...>
<data-columns>
<data-column [copyContent]="true" key="id" title="Id"></data-column>
<data-column key="created" title="Created" class="hidden"></data-column>
</data-columns>
</adf-tasklist>
```
Once set up, the sticky header behaves as shown in the image below:
![](../../docassets/images/datatable-sticky-header.png)

View File

@ -0,0 +1,36 @@
---
Title: Copy Clipboard directive
Added: v3.2.0
Status: Active
Last reviewed: 2019-04-01
---
# [Clipboard directive](../../../lib/core/clipboard/clipboard.directive.ts "Defined in clipboard.directive.ts")
Copy text to clipboard
## Basic Usage
```html
<span adf-clipboard="translate_key" [clipboard-notification]="notify message">
text to copy
</span>
<button adf-clipboard="translate_key" target="ref" [clipboard-notification]="notify message">
Copy
</button>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| target | `HTMLElement ref` | false | HTMLElement reference |
| clipboard-notification | `string` | | Translation key message for toast notification |
## Details
When the user hover the directive element a tooltip will will show up to inform that, when you click on the current element, the content or the reference content will be copied into the clipboard.

View File

@ -31,7 +31,7 @@
readonly="readonly">
<mat-icon class="adf-input-action" matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate"
[adf-clipboard]="sharedLinkInput">
[adf-clipboard] target="sharedLinkInput">
link
</mat-icon>
</mat-form-field>

View File

@ -1,6 +1,8 @@
@mixin adf-document-list-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary: map-get($theme, primary);
.mat-icon.adf-datatable-selected {
height: 100%;
@ -186,4 +188,14 @@
}
}
}
.adf-datatable-copy-tooltip {
position: absolute;
background: mat-color($primary);
color: mat-color($primary, default-contrast) !important;
padding: 5px 10px;
border-radius: 5px;
bottom: 88%;
left:0;
}
}

View File

@ -15,28 +15,30 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreModule } from '../core.module';
import { ClipboardService } from './clipboard.service';
import { ClipboardDirective } from './clipboard.directive';
import { RouterTestingModule } from '@angular/router/testing';
@Component({
selector: 'adf-test-component',
template: `
<button
clipboard-notification="copy success"
[adf-clipboard]="ref">
[adf-clipboard] [target]="ref">
copy
</button>
<input #ref />
`
})
class TestComponent {}
class TestTargetClipboardComponent {}
describe('ClipboardDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let fixture: ComponentFixture<TestTargetClipboardComponent>;
let clipboardService: ClipboardService;
setupTestBed({
@ -44,7 +46,7 @@ describe('ClipboardDirective', () => {
CoreModule.forRoot()
],
declarations: [
TestComponent
TestTargetClipboardComponent
],
providers: [
ClipboardService
@ -52,7 +54,7 @@ describe('ClipboardDirective', () => {
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
fixture = TestBed.createComponent(TestTargetClipboardComponent);
clipboardService = TestBed.get(ClipboardService);
fixture.detectChanges();
});
@ -65,3 +67,73 @@ describe('ClipboardDirective', () => {
expect(clipboardService.copyToClipboard).toHaveBeenCalled();
});
});
describe('CopyClipboardDirective', () => {
@Component({
selector: 'adf-copy-conent-test-component',
template: `<span adf-clipboard='DOCUMENT_LIST.ACTIONS.DOCUMENT.CLICK_TO_COPY'>{{ mockText }}</span>`
})
class TestCopyClipboardComponent {
mockText = 'text to copy';
@ViewChild(ClipboardDirective)
clipboardDirective: ClipboardDirective;
}
let fixture: ComponentFixture<TestCopyClipboardComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
CoreModule.forRoot(),
RouterTestingModule
],
declarations: [
TestCopyClipboardComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestCopyClipboardComponent);
element = fixture.debugElement.nativeElement;
fixture.detectChanges();
});
it('should show tooltip when hover element', (() => {
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).not.toBeNull();
}));
it('should not show tooltip when element it is not hovered', (() => {
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
expect(fixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).not.toBeNull();
spanHTMLElement.dispatchEvent(new Event('mouseleave'));
expect(fixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).toBeNull();
}));
it('should copy the content of element when click it', fakeAsync(() => {
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span');
fixture.detectChanges();
spyOn(document, 'execCommand');
spanHTMLElement.dispatchEvent(new Event('click'));
tick();
fixture.detectChanges();
expect(document.execCommand).toHaveBeenCalledWith('copy');
}));
it('should not copy the content of element when click it', fakeAsync(() => {
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span');
fixture.detectChanges();
spyOn(document, 'execCommand');
spanHTMLElement.dispatchEvent(new Event('mouseleave'));
tick();
fixture.detectChanges();
expect(document.execCommand).not.toHaveBeenCalled();
}));
});

View File

@ -15,33 +15,78 @@
* limitations under the License.
*/
import { Directive, Input, HostListener } from '@angular/core';
import { Directive, Input, HostListener, Component, ViewContainerRef, ComponentFactoryResolver, AfterContentInit } from '@angular/core';
import { ClipboardService } from './clipboard.service';
@Directive({
selector: '[adf-clipboard]',
exportAs: 'adfClipboard'
})
export class ClipboardDirective {
export class ClipboardDirective implements AfterContentInit {
// tslint:disable-next-line:no-input-rename
@Input('adf-clipboard') target: HTMLInputElement | HTMLTextAreaElement;
@Input('adf-clipboard')
placeholder: string;
@Input()
target: HTMLInputElement | HTMLTextAreaElement;
// tslint:disable-next-line:no-input-rename
@Input('clipboard-notification') message: string;
private value: string;
constructor(private clipboardService: ClipboardService,
public viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver) {}
@HostListener('click', ['$event'])
handleClickEvent(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.copyToClipboard();
}
constructor(private clipboardService: ClipboardService) {}
@HostListener('mouseenter')
showTooltip() {
const componentFactory = this.resolver.resolveComponentFactory(ClipboardComponent);
const componentRef = this.viewContainerRef.createComponent(componentFactory).instance;
componentRef.copyText = this.value;
componentRef.placeholder = this.placeholder;
}
@HostListener('mouseleave')
closeTooltip() {
this.viewContainerRef.remove();
}
private copyToClipboard() {
const isValidTarget = this.clipboardService.isTargetValid(this.target);
if (isValidTarget) {
this.clipboardService.copyToClipboard(this.target, this.message);
} else {
this.copyContentToClipboard(this.viewContainerRef.element.nativeElement.innerHTML);
}
}
private copyContentToClipboard(content) {
this.clipboardService.copyContentToClipboard(content, this.message);
}
ngAfterContentInit() {
setTimeout( () => {
this.value = this.viewContainerRef.element.nativeElement.innerHTML;
});
}
}
@Component({
selector: 'adf-datatable-highlight-tooltip',
template: `
<span class='adf-datatable-copy-tooltip'>{{ placeholder | translate }} <b> {{ copyText }} </b></span>
`
})
export class ClipboardComponent {
copyText: string;
placeholder: string;
}

View File

@ -17,22 +17,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ClipboardDirective } from './clipboard.directive';
import { ClipboardDirective, ClipboardComponent } from './clipboard.directive';
import { ClipboardService } from './clipboard.service';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
imports: [
CommonModule
CommonModule,
TranslateModule.forChild()
],
providers: [
ClipboardService
],
declarations: [
ClipboardDirective
ClipboardDirective,
ClipboardComponent
],
exports: [
ClipboardDirective
]
],
entryComponents: [ClipboardComponent]
})
export class ClipboardModule {}

View File

@ -32,8 +32,6 @@ export class ClipboardService {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
return !target.hasAttribute('disabled');
}
this.logService.error(`${target} should be input or textarea`);
return false;
}
@ -50,6 +48,20 @@ export class ClipboardService {
}
}
copyContentToClipboard(content: string, message: string) {
try {
document.addEventListener('copy', (e: ClipboardEvent) => {
e.clipboardData.setData('text/plain', (content));
e.preventDefault();
document.removeEventListener('copy', null);
});
document.execCommand('copy');
this.notify(message);
} catch (error) {
this.logService.error(error);
}
}
private notify(message) {
if (message) {
this.notificationService.openSnackMessage(message);

View File

@ -66,6 +66,10 @@ export class DataColumnComponent implements OnInit {
@Input('class')
cssClass: string;
/** flag to show the copy content directive */
@Input()
copyContent: boolean;
ngOnInit() {
if (!this.srTitle && this.key === '$thumbnail') {
this.srTitle = 'Thumbnail';

View File

@ -35,13 +35,21 @@ import { Node } from '@alfresco/js-api';
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ng-container>
<span *ngIf="copyContent; else defaultCell"
adf-clipboard="DATATABLE.CLICK_TO_COPY"
[clipboard-notification]="'DATATABLE.SUCCESS_COPY'"
[attr.aria-label]="value$ | async"
[title]="tooltip"
class="adf-datatable-cell-value"
>{{ value$ | async }}</span>
</ng-container>
<ng-template #defaultCell>
<span
[attr.aria-label]="value$ | async"
[title]="tooltip"
class="adf-datatable-cell-value"
>{{ value$ | async }}</span
>
</ng-container>
>{{ value$ | async }}</span>
</ng-template>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-datatable-cell' }
@ -58,6 +66,9 @@ export class DataTableCellComponent implements OnInit, OnDestroy {
value$ = new BehaviorSubject<any>('');
@Input()
copyContent: boolean;
@Input()
tooltip: string;
@ -67,7 +78,6 @@ export class DataTableCellComponent implements OnInit, OnDestroy {
ngOnInit() {
this.updateValue();
this.sub = this.alfrescoApiService.nodeUpdated.subscribe((node: Node) => {
if (this.row) {
const { entry } = this.row['node'];

View File

@ -148,6 +148,7 @@
<div *ngSwitchCase="'text'" class="adf-cell-value"
[attr.data-automation-id]="'text_' + data.getValue(row, col)">
<adf-datatable-cell
[copyContent]="col.copyContent"
[data]="data"
[column]="col"
[row]="row"

View File

@ -216,6 +216,7 @@
&--text {
text-align: left;
position: relative;
}
&--date {

View File

@ -27,4 +27,5 @@ export interface DataColumn {
cssClass?: string;
template?: TemplateRef<any>;
formatTooltip?: Function;
copyContent?: boolean;
}

View File

@ -29,6 +29,7 @@ export class ObjectDataColumn implements DataColumn {
srTitle: string;
cssClass: string;
template?: TemplateRef<any>;
copyContent?: boolean;
constructor(input: any) {
this.key = input.key;
@ -39,5 +40,6 @@ export class ObjectDataColumn implements DataColumn {
this.srTitle = input.srTitle;
this.cssClass = input.cssClass;
this.template = input.template;
this.copyContent = input.copyContent;
}
}

View File

@ -41,6 +41,7 @@ import { CustomEmptyContentTemplateDirective } from './directives/custom-empty-c
import { CustomLoadingContentTemplateDirective } from './directives/custom-loading-template.directive';
import { CustomNoPermissionTemplateDirective } from './directives/custom-no-permission-template.directive';
import { JsonCellComponent } from './components/datatable/json-cell.component';
import { ClipboardModule } from '../clipboard/clipboard.module';
@NgModule({
imports: [
@ -50,7 +51,8 @@ import { JsonCellComponent } from './components/datatable/json-cell.component';
TranslateModule.forChild(),
ContextMenuModule,
PipeModule,
DirectiveModule
DirectiveModule,
ClipboardModule
],
declarations: [
DataTableComponent,
@ -88,5 +90,6 @@ import { JsonCellComponent } from './components/datatable/json-cell.component';
CustomLoadingContentTemplateDirective,
CustomNoPermissionTemplateDirective
]
})
export class DataTableModule {}

View File

@ -36,10 +36,6 @@ describe('ClaimTaskDirective', () => {
@ViewChild(ClaimTaskDirective)
claimTaskDirective: ClaimTaskDirective;
onCompleteTask(event: any) {
return event;
}
}
let fixture: ComponentFixture<TestComponent>;

View File

@ -55,6 +55,20 @@ class CustomTaskListComponent {
})
class EmptyTemplateComponent {
}
@Component({
template: `
<adf-cloud-task-list #taskListCloudCopy>
<data-columns>
<data-column [copyContent]="true" key="entry.id" title="ADF_CLOUD_TASK_LIST.PROPERTIES.ID"></data-column>
<data-column key="entry.name" title="ADF_CLOUD_TASK_LIST.PROPERTIES.NAME"></data-column>
</data-columns>
</adf-cloud-task-list>`
})
class CustomCopyContentTaskListComponent {
@ViewChild(TaskListCloudComponent)
taskList: TaskListCloudComponent;
}
describe('TaskListCloudComponent', () => {
let component: TaskListCloudComponent;
let fixture: ComponentFixture<TaskListCloudComponent>;
@ -228,21 +242,29 @@ describe('TaskListCloudComponent', () => {
describe('Injecting custom colums for tasklist - CustomTaskListComponent', () => {
let fixtureCustom: ComponentFixture<CustomTaskListComponent>;
let componentCustom: CustomTaskListComponent;
let customCopyComponent: CustomCopyContentTaskListComponent;
let element: any;
let copyFixture: ComponentFixture<CustomCopyContentTaskListComponent>;
setupTestBed({
imports: [CoreModule.forRoot()],
declarations: [TaskListCloudComponent, CustomTaskListComponent],
declarations: [TaskListCloudComponent, CustomTaskListComponent, CustomCopyContentTaskListComponent],
providers: [TaskListCloudService]
});
beforeEach(() => {
spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTask));
fixtureCustom = TestBed.createComponent(CustomTaskListComponent);
copyFixture = TestBed.createComponent(CustomCopyContentTaskListComponent);
fixtureCustom.detectChanges();
componentCustom = fixtureCustom.componentInstance;
customCopyComponent = copyFixture.componentInstance;
element = copyFixture.debugElement.nativeElement;
});
afterEach(() => {
fixtureCustom.destroy();
copyFixture.destroy();
});
it('should create instance of CustomTaskListComponent', () => {
@ -257,6 +279,37 @@ describe('TaskListCloudComponent', () => {
expect(componentCustom.taskList.columns.length).toEqual(3);
});
it('it should show copy tooltip when key is present in data-colunn', async(() => {
copyFixture.detectChanges();
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
copyFixture.whenStable().then(() => {
copyFixture.detectChanges();
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
copyFixture.detectChanges();
expect(copyFixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).not.toBeNull();
});
customCopyComponent.taskList.appName = appName.currentValue;
customCopyComponent.taskList.ngOnChanges({ 'appName': appName });
copyFixture.detectChanges();
}));
it('it should not show copy tooltip when key is not present in data-colunn', async(() => {
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
customCopyComponent.taskList.success.subscribe( () => {
copyFixture.whenStable().then(() => {
copyFixture.detectChanges();
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span[title="standalone-subtask"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
copyFixture.detectChanges();
expect(copyFixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).toBeNull();
});
});
customCopyComponent.taskList.appName = appName.currentValue;
customCopyComponent.taskList.ngOnChanges({ 'appName': appName });
copyFixture.detectChanges();
}));
});
describe('Creating an empty custom template - EmptyTemplateComponent', () => {
@ -285,4 +338,88 @@ describe('TaskListCloudComponent', () => {
});
}));
});
describe('Copy cell content directive from app.config specifications', () => {
let element: any;
let taskSpy: jasmine.Spy;
setupTestBed({
imports: [ProcessServiceCloudTestingModule, TaskListCloudModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
beforeEach( () => {
appConfig = TestBed.get(AppConfigService);
taskListCloudService = TestBed.get(TaskListCloudService);
appConfig.config = Object.assign(appConfig.config, {
'adf-cloud-task-list': {
'presets': {
'fakeCustomSchema': [
{
'key': 'entry.id',
'type': 'text',
'title': 'ADF_CLOUD_TASK_LIST.PROPERTIES.FAKE',
'sortable': true,
'copyContent': true
},
{
'key': 'entry.name',
'type': 'text',
'title': 'ADF_CLOUD_TASK_LIST.PROPERTIES.TASK_FAKE',
'sortable': true
}
]
}
}
});
fixture = TestBed.createComponent(TaskListCloudComponent);
component = fixture.componentInstance;
element = fixture.debugElement.nativeElement;
taskSpy = spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(fakeGlobalTask));
});
afterEach(() => {
fixture.destroy();
});
it('shoud show tooltip if config copyContent flag is true', async(() => {
taskSpy.and.returnValue(of(fakeGlobalTask));
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
component.success.subscribe( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).not.toBeNull();
});
});
component.presetColumn = 'fakeCustomSchema';
component.appName = appName.currentValue;
component.ngOnChanges({ 'appName': appName });
component.ngAfterContentInit();
}));
it('shoud not show tooltip if config copyContent flag is true', async(() => {
taskSpy.and.returnValue(of(fakeGlobalTask));
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
component.success.subscribe( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
const spanHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('span[title="standalone-subtask"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-datatable-copy-tooltip')).toBeNull();
});
});
component.presetColumn = 'fakeCustomSchema';
component.appName = appName.currentValue;
component.ngOnChanges({ 'appName': appName });
component.ngAfterContentInit();
}));
});
});