refactor(docs-infra): use CDK clipboard service (#40840)

The CDK has had a service for copying strings to the clipboard. These changes switch
AIO to it, rather than having to maintain a custom solution.

PR Close #40840
This commit is contained in:
Kristiyan Kostadinov 2021-02-14 10:47:58 +01:00 committed by Joey Perrott
parent 5f0c219883
commit f2ee9d5679
4 changed files with 15 additions and 116 deletions

View File

@ -3,10 +3,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Clipboard } from '@angular/cdk/clipboard';
import { CodeComponent } from './code.component';
import { CodeModule } from './code.module';
import { CopierService } from 'app/shared//copier.service';
import { Logger } from 'app/shared/logger.service';
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
import { PrettyPrinter } from './pretty-printer.service';
@ -32,7 +32,6 @@ describe('CodeComponent', () => {
imports: [ NoopAnimationsModule, CodeModule ],
declarations: [ HostComponent ],
providers: [
CopierService,
{ provide: Logger, useClass: TestLogger },
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
]
@ -222,23 +221,23 @@ describe('CodeComponent', () => {
});
it('should call copier service when clicked', () => {
const copierService: CopierService = TestBed.inject(CopierService);
const spy = spyOn(copierService, 'copyText');
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
expect(spy.calls.count()).toBe(0, 'before click');
getButton().click();
expect(spy.calls.count()).toBe(1, 'after click');
});
it('should copy code text when clicked', () => {
const copierService: CopierService = TestBed.inject(CopierService);
const spy = spyOn(copierService, 'copyText');
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
getButton().click();
expect(spy.calls.argsFor(0)[0]).toBe(oneLineCode, 'after click');
});
it('should preserve newlines in the copied code', () => {
const copierService: CopierService = TestBed.inject(CopierService);
const spy = spyOn(copierService, 'copyText');
const clipboard = TestBed.inject(Clipboard);
const spy = spyOn(clipboard, 'copy');
const expectedCode = smallMultiLineCode.trim().replace(/&lt;/g, '<').replace(/&gt;/g, '>');
let actualCode;
@ -259,19 +258,19 @@ describe('CodeComponent', () => {
it('should display a message when copy succeeds', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const copierService: CopierService = TestBed.inject(CopierService);
const clipboard = TestBed.inject(Clipboard);
spyOn(snackBar, 'open');
spyOn(copierService, 'copyText').and.returnValue(true);
spyOn(clipboard, 'copy').and.returnValue(true);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
});
it('should display an error when copy fails', () => {
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
const copierService: CopierService = TestBed.inject(CopierService);
const clipboard = TestBed.inject(Clipboard);
const logger = TestBed.inject(Logger) as unknown as TestLogger;
spyOn(snackBar, 'open');
spyOn(copierService, 'copyText').and.returnValue(false);
spyOn(clipboard, 'copy').and.returnValue(false);
getButton().click();
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
expect(logger.error).toHaveBeenCalledTimes(1);

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { Clipboard } from '@angular/cdk/clipboard';
import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service';
import { CopierService } from 'app/shared/copier.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { tap } from 'rxjs/operators';
@ -96,7 +96,7 @@ export class CodeComponent implements OnChanges {
constructor(
private snackbar: MatSnackBar,
private pretty: PrettyPrinter,
private copier: CopierService,
private clipboard: Clipboard,
private logger: Logger) {}
ngOnChanges() {
@ -144,7 +144,7 @@ export class CodeComponent implements OnChanges {
/** Copies the code snippet to the user's clipboard. */
doCopy() {
const code = this.codeText;
const successfullyCopied = this.copier.copyText(code);
const successfullyCopied = this.clipboard.copy(code);
if (successfullyCopied) {
this.logger.log('Copied code to clipboard:', code);

View File

@ -3,13 +3,12 @@ import { CommonModule } from '@angular/common';
import { CodeComponent } from './code.component';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { PrettyPrinter } from './pretty-printer.service';
import { CopierService } from 'app/shared/copier.service';
@NgModule({
imports: [ CommonModule, MatSnackBarModule ],
declarations: [ CodeComponent ],
entryComponents: [ CodeComponent ],
exports: [ CodeComponent ],
providers: [ PrettyPrinter, CopierService ]
providers: [ PrettyPrinter ]
})
export class CodeModule { }

View File

@ -1,99 +0,0 @@
/**
* This class is based on the code in the following projects:
*
* - https://github.com/zenorocha/select
* - https://github.com/zenorocha/clipboard.js/
*
* Both released under MIT license - © Zeno Rocha
*
* It is also influenced by the Angular CDK `PendingCopy` class:
* https://github.com/angular/components/blob/master/src/cdk/clipboard/pending-copy.ts
*/
export class CopierService {
/**
* Copy the contents of a `<textarea>` element to the clipboard.
*
* NOTE: For this method to work, the elements must be already inserted into the DOM.
*
* @param textArea The area containing the text to be copied to the clipboard.
* @return Whether the copy operation was successful.
*/
private copyTextArea(textArea: HTMLTextAreaElement): boolean {
const currentFocus = document.activeElement as HTMLOrSVGElement | null;
try {
textArea.select();
textArea.setSelectionRange(0, textArea.value.length);
return document.execCommand('copy');
} catch {
return false;
} finally {
// Calling `.select()` on the `<textarea>` element may have also focused it.
// Change the focus back to the previously focused element.
currentFocus?.focus();
}
}
/**
* Create a temporary, hidden `<textarea>` element and set its value to the specified text.
*
* @param text The text to be inserted into the textarea.
* @return The temporary `<textarea>` element containing the specified text.
*/
private createTextArea(text: string): HTMLTextAreaElement {
const docElem = document.documentElement;
const isRTL = docElem.getAttribute('dir') === 'rtl';
// Create a temporary element to hold the contents to copy.
const textArea = document.createElement('textarea');
const style = textArea.style;
// Prevent zooming on iOS.
style.fontSize = '12pt';
// Reset box model.
style.border = '0';
style.padding = '0';
style.margin = '0';
// Make the element invisible and move it out of screen horizontally.
style.opacity = '0';
style.position = 'fixed';
style.top = '0';
style[isRTL ? 'right' : 'left'] = '-999em';
textArea.setAttribute('aria-hidden', 'true');
textArea.setAttribute('readonly', '');
textArea.value = text;
return textArea;
}
/**
* Copy the specified text to the clipboard.
*
* @param text The text to be copied to the clipboard.
* @return Whether the copy operation was successful.
*/
copyText(text: string): boolean {
// Create a `<textarea>` element with the specified text.
const textArea = this.createTextArea(text);
// Insert it into the DOM.
document.body.appendChild(textArea);
// Copy its contents to the clipboard.
const success = this.copyTextArea(textArea);
// Remove it from the DOM, so it can be garbage-collected.
if (textArea.parentNode) {
// We cannot use ChildNode.remove() because of IE11.
textArea.parentNode.removeChild(textArea);
}
return success;
}
}