From 062c1a16262f0eb31c2aca9cc3ac61ff23c59615 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 24 May 2017 21:42:06 -0700 Subject: [PATCH] added copy-to-clipboard directive that does not require Flash --- package.json | 1 + static/css/quay.css | 2 +- static/directives/build-logs-view.html | 3 +- static/directives/copy-box.html | 11 +-- static/directives/fetch-tag-dialog.html | 8 +- static/js/directives/ui/build-logs-view.js | 4 - .../clipboard-copy.directive.spec.ts | 77 +++++++++++++++++++ .../clipboard-copy.directive.ts | 63 +++++++++++++++ static/js/directives/ui/copy-box.js | 53 +------------ static/js/directives/ui/fetch-tag-dialog.js | 1 - static/js/quay.module.ts | 4 + webpack.config.js | 7 +- yarn.lock | 26 +++++++ 13 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts create mode 100644 static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts diff --git a/package.json b/package.json index 334d22356..471269a4f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "bootstrap": "^3.3.2", "bootstrap-datepicker": "^1.6.4", "cal-heatmap": "^3.3.10", + "clipboard": "^1.6.1", "core-js": "^2.4.1", "d3": "^3.3.3", "eonasdan-bootstrap-datetimepicker": "^4.17.43", diff --git a/static/css/quay.css b/static/css/quay.css index bf3f07000..c7ffa24f2 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1569,7 +1569,7 @@ p.editable:hover i { transition: color 0.5s ease-in-out; } -.copy-box-element .copy-container .copy-icon.zeroclipboard-is-hover { +.copy-box-element .copy-container .copy-icon:hover { color: #444; } diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html index f1574a602..f939caa95 100644 --- a/static/directives/build-logs-view.html +++ b/static/directives/build-logs-view.html @@ -1,6 +1,7 @@
- diff --git a/static/directives/copy-box.html b/static/directives/copy-box.html index 7532a6d68..79b033059 100644 --- a/static/directives/copy-box.html +++ b/static/directives/copy-box.html @@ -1,11 +1,12 @@
- - + +
diff --git a/static/directives/fetch-tag-dialog.html b/static/directives/fetch-tag-dialog.html index 0cb45f2d1..168016459 100644 --- a/static/directives/fetch-tag-dialog.html +++ b/static/directives/fetch-tag-dialog.html @@ -66,17 +66,15 @@
Command: -
{{ getCommand(currentFormat, currentRobot) }}
+
{{ getCommand(currentFormat, currentRobot) }}
diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js index 714a42bc3..15bad6680 100644 --- a/static/js/directives/ui/build-logs-view.js +++ b/static/js/directives/ui/build-logs-view.js @@ -23,10 +23,6 @@ angular.module('quay').directive('buildLogsView', function () { repoStatusApiCall = ApiService.getRepoBuildStatusSuperUser; repoLogApiCall = ApiService.getRepoBuildLogsSuperUserAsResource; } - var result = $element.find('#copyButton').clipboardCopy(); - if (!result) { - $element.find('#copyButton').hide(); - } $scope.logEntries = null; $scope.currentParentEntry = null; diff --git a/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts new file mode 100644 index 000000000..7080dc6b2 --- /dev/null +++ b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts @@ -0,0 +1,77 @@ +import { ClipboardCopyDirective } from './clipboard-copy.directive'; +import * as Clipboard from 'clipboard'; +import { Mock } from 'ts-mocks'; +import Spy = jasmine.Spy; + + +describe("ClipboardCopyDirective", () => { + var directive: ClipboardCopyDirective; + var $elementMock: any; + var $timeoutMock: any; + var $documentMock: any; + var clipboardFactory: any; + var clipboardMock: Mock; + + beforeEach(() => { + $elementMock = new Mock(); + $timeoutMock = jasmine.createSpy('$timeoutSpy').and.callFake((fn: () => void, delay) => fn()); + $documentMock = new Mock(); + clipboardMock = new Mock(); + clipboardMock.setup(mock => mock.on).is((eventName: string, callback: (event) => void) => {}); + clipboardFactory = jasmine.createSpy('clipboardFactory').and.returnValue(clipboardMock.Object); + directive = new ClipboardCopyDirective([$elementMock.Object], + $timeoutMock, + [$documentMock.Object], + clipboardFactory); + directive.copyTargetSelector = "#copy-input-box-0"; + }); + + describe("ngAfterContentInit", () => { + + it("initializes new Clipboard instance", () => { + const target = new Mock(); + $documentMock.setup(mock => mock.querySelector).is(selector => target.Object); + directive.ngAfterContentInit(); + + expect(clipboardFactory).toHaveBeenCalled(); + expect((clipboardFactory.calls.argsFor(0)[0])).toEqual($elementMock.Object); + expect((clipboardFactory.calls.argsFor(0)[1]['target']())).toEqual(target.Object); + }); + + it("sets error callback for Clipboard instance", () => { + directive.ngAfterContentInit(); + + expect((clipboardMock.Object.on.calls.argsFor(0)[0])).toEqual('error'); + expect((clipboardMock.Object.on.calls.argsFor(0)[1])).toBeDefined(); + }); + + it("sets success callback for Clipboard instance", (done) => { + directive.ngAfterContentInit(); + + expect((clipboardMock.Object.on.calls.argsFor(1)[0])).toEqual('success'); + expect((clipboardMock.Object.on.calls.argsFor(1)[1])).toBeDefined(); + done(); + }); + }); + + describe("ngOnDestroy", () => { + + beforeEach(() => { + clipboardMock.setup(mock => mock.destroy).is(() => null); + }); + + it("calls method to destroy Clipboard instance if set", (done) => { + directive.ngAfterContentInit(); + directive.ngOnDestroy(); + + expect((clipboardMock.Object.destroy)).toHaveBeenCalled(); + done(); + }); + + it("does not call method to destroy Clipboard instance if not set", () => { + directive.ngOnDestroy(); + + expect((clipboardMock.Object.destroy)).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts new file mode 100644 index 000000000..0be17118b --- /dev/null +++ b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts @@ -0,0 +1,63 @@ +import { Directive, Inject, Input, AfterContentInit, OnDestroy } from 'ng-metadata/core'; +import * as Clipboard from 'clipboard'; + + +@Directive({ + selector: '[clipboardCopy]' +}) +export class ClipboardCopyDirective implements AfterContentInit, OnDestroy { + + @Input('@clipboardCopy') public copyTargetSelector: string; + + private clipboard: Clipboard; + + constructor(@Inject('$element') private $element: ng.IAugmentedJQuery, + @Inject('$timeout') private $timeout: ng.ITimeoutService, + @Inject('$document') private $document: ng.IDocumentService, + @Inject('clipboardFactory') private clipboardFactory: (elem, options) => Clipboard) { + + } + + public ngAfterContentInit(): void { + // FIXME: Need to wait for DOM to render to find target element + this.$timeout(() => { + this.clipboard = this.clipboardFactory(this.$element[0], {target: (trigger) => { + return this.$document[0].querySelector(this.copyTargetSelector); + }}); + + this.clipboard.on("error", (e) => { + console.error(e); + }); + + this.clipboard.on('success', (e) => { + const container = e.trigger.parentNode.parentNode.parentNode; + const messageElem = container.querySelector('.clipboard-copied-message'); + if (!messageElem) { + return; + } + + // Resets the animation. + var elem = messageElem; + elem.style.display = 'none'; + elem.classList.remove('animated'); + + // Show the notification. + setTimeout(() => { + elem.style.display = 'inline-block'; + elem.classList.add('animated'); + }, 10); + + // Reset the notification. + setTimeout(() => { + elem.style.display = 'none'; + }, 5000); + }); + }, 100); + } + + public ngOnDestroy(): void { + if (this.clipboard) { + this.clipboard.destroy(); + } + } +} \ No newline at end of file diff --git a/static/js/directives/ui/copy-box.js b/static/js/directives/ui/copy-box.js index 0bb61f5fa..63cb69cb9 100644 --- a/static/js/directives/ui/copy-box.js +++ b/static/js/directives/ui/copy-box.js @@ -1,49 +1,5 @@ -$.fn.clipboardCopy = function() { - if (__zeroClipboardSupported) { - (new ZeroClipboard($(this))); - return true; - } - - this.hide(); - return false; -}; - -// Initialize the clipboard system. -(function () { - __zeroClipboardSupported = true; - - ZeroClipboard.on("error", function(e) { - __zeroClipboardSupported = false; - }); - - ZeroClipboard.on('aftercopy', function(e) { - var container = e.target.parentNode.parentNode.parentNode; - var message = $(container).find('.clipboard-copied-message')[0]; - if (!message) { - return; - } - - // Resets the animation. - var elem = message; - elem.style.display = 'none'; - elem.classList.remove('animated'); - - // Show the notification. - setTimeout(function() { - elem.style.display = 'inline-block'; - elem.classList.add('animated'); - }, 10); - - // Reset the notification. - setTimeout(function() { - elem.style.display = 'none'; - }, 5000); - }); -})(); - /** - * An element which displays a textfield with a "Copy to Clipboard" icon next to it. Note - * that this method depends on the clipboard copying library in the lib/ folder. + * An element which displays a textfield with a "Copy to Clipboard" icon next to it. */ angular.module('quay').directive('copyBox', function () { var directiveDefinitionObject = { @@ -62,13 +18,6 @@ angular.module('quay').directive('copyBox', function () { var number = $rootScope.__copyBoxIdCounter || 0; $rootScope.__copyBoxIdCounter = number + 1; $scope.inputId = "copy-box-input-" + number; - - var button = $($element).find('.copy-icon'); - var input = $($element).find('input'); - - input.attr('id', $scope.inputId); - button.attr('data-clipboard-target', $scope.inputId); - $scope.disabled = !button.clipboardCopy(); } }; return directiveDefinitionObject; diff --git a/static/js/directives/ui/fetch-tag-dialog.js b/static/js/directives/ui/fetch-tag-dialog.js index 0ae6505ef..ae84b0f6d 100644 --- a/static/js/directives/ui/fetch-tag-dialog.js +++ b/static/js/directives/ui/fetch-tag-dialog.js @@ -125,7 +125,6 @@ angular.module('quay').directive('fetchTagDialog', function () { updateFormats(); - $element.find('#copyClipboard').clipboardCopy(); $element.find('#fetchTagDialog').modal({}); } }; diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index b90dffc49..cce2b51da 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -36,7 +36,9 @@ import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-tool import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component'; import { BrowserPlatform, browserPlatform } from './constants/platform.constant'; import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component'; +import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive'; import { Converter, ConverterOptions } from 'showdown'; +import * as Clipboard from 'clipboard'; /** @@ -75,6 +77,7 @@ import { Converter, ConverterOptions } from 'showdown'; CorTabComponent, CorTabPaneComponent, ManageTriggerComponent, + ClipboardCopyDirective, ], providers: [ ViewArrayImpl, @@ -86,6 +89,7 @@ import { Converter, ConverterOptions } from 'showdown'; {provide: 'markdownConverterFactory', useValue: (options?: ConverterOptions) => new Converter(options)}, {provide: 'BrowserPlatform', useValue: browserPlatform}, {provide: 'CorTabCurrentHandlerFactory', useValue: CorTabCurrentHandlerFactory}, + {provide: 'clipboardFactory', useValue: (trigger, options) => new Clipboard(trigger, options)}, ], }) export class QuayModule { diff --git a/webpack.config.js b/webpack.config.js index bf45ba6f5..e823b1e29 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,9 +22,7 @@ var config = { rules: [ { test: /\.tsx?$/, - use: [ - "ts-loader", - ], + use: ["ts-loader"], exclude: /node_modules/ }, { @@ -41,13 +39,12 @@ var config = { 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname)), 'html-loader', ] - } + }, ] }, plugins: [ // Replace references to global variables with associated modules new webpack.ProvidePlugin({ - ZeroClipboard: 'zeroclipboard', FileSaver: 'file-saver', angular: "angular", $: "jquery", diff --git a/yarn.lock b/yarn.lock index 5f1c16a57..fd154515a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -651,6 +651,14 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" +clipboard@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53" + dependencies: + good-listener "^1.2.0" + select "^1.1.2" + tiny-emitter "^1.0.0" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -1038,6 +1046,10 @@ delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +delegate@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe" + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -1567,6 +1579,12 @@ globule@^1.0.0: lodash "~4.16.4" minimatch "~3.0.2" +good-listener@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + dependencies: + delegate "^3.1.2" + got@^5.0.0: version "5.7.1" resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" @@ -3645,6 +3663,10 @@ script-loader@^0.7.0: dependencies: raw-loader "~0.5.1" +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -3996,6 +4018,10 @@ timers-browserify@^2.0.2: dependencies: setimmediate "^1.0.4" +tiny-emitter@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.2.0.tgz#6dc845052cb08ebefc1874723b58f24a648c3b6f" + tmp@0.0.x: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"