From 22650621f2a93f60636d74ba45262f8bf5b77f36 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Tue, 20 Jul 2021 14:02:00 +0100 Subject: [PATCH] ui: Use native clipboard functionality, but keep fallback --- .../consul-ui/app/initializers/clipboard.js | 16 +++++ .../consul-ui/app/modifiers/with-copyable.js | 6 +- .../app/services/clipboard/native.js | 65 +++++++++++++++++++ .../consul-ui/app/services/clipboard/os.js | 9 --- .../app/services/clipboard/polyfill.js | 9 +++ ui/packages/consul-ui/ember-cli-build.js | 5 ++ .../lib/startup/templates/body.html.js | 1 + ui/packages/consul-ui/vendor/init.js | 3 + 8 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 ui/packages/consul-ui/app/initializers/clipboard.js create mode 100644 ui/packages/consul-ui/app/services/clipboard/native.js delete mode 100644 ui/packages/consul-ui/app/services/clipboard/os.js create mode 100644 ui/packages/consul-ui/app/services/clipboard/polyfill.js diff --git a/ui/packages/consul-ui/app/initializers/clipboard.js b/ui/packages/consul-ui/app/initializers/clipboard.js new file mode 100644 index 0000000000..6b13ebda19 --- /dev/null +++ b/ui/packages/consul-ui/app/initializers/clipboard.js @@ -0,0 +1,16 @@ +import NavigatorClipboard from 'consul-ui/services/clipboard/native'; +import ExecClipboard from 'consul-ui/services/clipboard/polyfill'; + +export function initialize(application) { + // which clipboard impl. depends on whether we have native support + if (!application.hasRegistration('service:clipboard')) { + application.register( + `service:clipboard`, + window.navigator.clipboard ? NavigatorClipboard : ExecClipboard + ); + } +} + +export default { + initialize, +}; diff --git a/ui/packages/consul-ui/app/modifiers/with-copyable.js b/ui/packages/consul-ui/app/modifiers/with-copyable.js index 562f058755..b061f28cde 100644 --- a/ui/packages/consul-ui/app/modifiers/with-copyable.js +++ b/ui/packages/consul-ui/app/modifiers/with-copyable.js @@ -6,7 +6,7 @@ const typeAssertion = (type, value, withDefault) => { return typeof value === type ? value : withDefault; }; export default class WithCopyableModifier extends Modifier { - @service('clipboard/os') clipboard; + @service('clipboard') clipboard; hash = null; source = null; @@ -19,7 +19,9 @@ export default class WithCopyableModifier extends Modifier { return typeAssertion('function', _hash.success, () => {})(e); }, error: e => { - runInDebug(_ => console.info(`with-copyable: Error copying \`${value}\``)); + runInDebug(_ => + console.info(`with-copyable: Error copying \`${value}\` - ${e.toString()}`) + ); return typeAssertion('function', _hash.error, () => {})(e); }, }; diff --git a/ui/packages/consul-ui/app/services/clipboard/native.js b/ui/packages/consul-ui/app/services/clipboard/native.js new file mode 100644 index 0000000000..b2745211ab --- /dev/null +++ b/ui/packages/consul-ui/app/services/clipboard/native.js @@ -0,0 +1,65 @@ +import Service from '@ember/service'; + +const map = new WeakMap(); +// we should only have one listener per event per element as we have a thin +// modifier layer over this. +// Event arrays are guaranteed to exist as the arrays are created at the same +// time as the functions themselves, if there are no arrays there are also no +// functions to be called. +const EVENTS = ['success', 'error']; +const addEventListener = function(eventName, cb) { + if (EVENTS.includes(eventName)) { + map.get(this)[eventName].push(cb); + } + return this; +}; +const removeEventListener = function(eventName, cb) { + if (EVENTS.includes(eventName)) { + let listeners = map.get(this)[eventName]; + const pos = listeners.findIndex(item => item === cb); + if (pos !== -1) { + listeners.splice(pos, 1); + } + } + return this; +}; +// +export default class OsService extends Service { + constructor(owner, clipboard = window.navigator.clipboard) { + super(...arguments); + this.clipboard = clipboard; + } + execute($el, options) { + // make a pseudo-target that follows the ClipboardJS emitter until we want + // to reverse the interface + const target = { + on: addEventListener, + off: removeEventListener, + }; + // we only want to support clicking/pressing enter + const click = e => { + const text = options.text(); + // ClipboardJS events also have action and trigger props + // but we don't use them + this.clipboard.writeText(text).then( + () => map.get(target).success.forEach(cb => cb({ text: text })), + e => map.get(target).error.forEach(cb => cb(e)) + ); + }; + // add all the events as empty arrays + map.set( + target, + EVENTS.reduce((prev, item) => { + prev[item] = []; + return prev; + }, {}) + ); + // listen plus remove using ClipboardJS interface + $el.addEventListener('click', click); + target.destroy = function() { + $el.removeEventListener('click', click); + map.delete(target); + }; + return target; + } +} diff --git a/ui/packages/consul-ui/app/services/clipboard/os.js b/ui/packages/consul-ui/app/services/clipboard/os.js deleted file mode 100644 index 99dc5cd41c..0000000000 --- a/ui/packages/consul-ui/app/services/clipboard/os.js +++ /dev/null @@ -1,9 +0,0 @@ -import Service from '@ember/service'; - -import Clipboard from 'clipboard'; - -export default class OsService extends Service { - execute() { - return new Clipboard(...arguments); - } -} diff --git a/ui/packages/consul-ui/app/services/clipboard/polyfill.js b/ui/packages/consul-ui/app/services/clipboard/polyfill.js new file mode 100644 index 0000000000..25734dec55 --- /dev/null +++ b/ui/packages/consul-ui/app/services/clipboard/polyfill.js @@ -0,0 +1,9 @@ +/* global ClipboardJS*/ +import Service from '@ember/service'; + +export default class PolyfillService extends Service { + execute() { + // Access the ClipboardJS lib see vendor/init.js for polyfill loading + return new ClipboardJS(...arguments); + } +} diff --git a/ui/packages/consul-ui/ember-cli-build.js b/ui/packages/consul-ui/ember-cli-build.js index cd993ed90f..c4b8330495 100644 --- a/ui/packages/consul-ui/ember-cli-build.js +++ b/ui/packages/consul-ui/ember-cli-build.js @@ -136,6 +136,11 @@ module.exports = function(defaults, $ = process.env) { // CSS.escape polyfill app.import('node_modules/css.escape/css.escape.js', { outputFile: 'assets/css.escape.js' }); + // Clipboard ponyfill + app.import('node_modules/clipboard/dist/clipboard.js', { + outputFile: 'assets/clipboard/clipboard.js', + }); + // JSON linting support. Possibly dynamically loaded via CodeMirror linting. See components/code-editor.js app.import('node_modules/jsonlint/lib/jsonlint.js', { outputFile: 'assets/codemirror/mode/javascript/javascript.js', diff --git a/ui/packages/consul-ui/lib/startup/templates/body.html.js b/ui/packages/consul-ui/lib/startup/templates/body.html.js index 975cbd6506..5e3c033bd8 100644 --- a/ui/packages/consul-ui/lib/startup/templates/body.html.js +++ b/ui/packages/consul-ui/lib/startup/templates/body.html.js @@ -36,6 +36,7 @@ ${environment === 'production' ? `{{jsonEncode .}}` : JSON.stringify(config.oper "text-encoding/encoding-indexes.js": "${rootURL}assets/encoding-indexes.js", "text-encoding/encoding.js": "${rootURL}assets/encoding.js", "css.escape/css.escape.js": "${rootURL}assets/css.escape.js", + "clipboard/clipboard.js": "${rootURL}assets/clipboard/clipboard.js", "codemirror/mode/javascript/javascript.js": "${rootURL}assets/codemirror/mode/javascript/javascript.js", "codemirror/mode/ruby/ruby.js": "${rootURL}assets/codemirror/mode/ruby/ruby.js", "codemirror/mode/yaml/yaml.js": "${rootURL}assets/codemirror/mode/yaml/yaml.js" diff --git a/ui/packages/consul-ui/vendor/init.js b/ui/packages/consul-ui/vendor/init.js index cefbbb81f7..f239b3adb2 100644 --- a/ui/packages/consul-ui/vendor/init.js +++ b/ui/packages/consul-ui/vendor/init.js @@ -16,6 +16,9 @@ if (!(window.CSS && window.CSS.escape)) { appendScript(fs.get(`${['css.escape', 'css.escape'].join('/')}.js`)); } + if (!window.navigator.clipboard) { + appendScript(fs.get(`${['clipboard', 'clipboard'].join('/')}.js`)); + } try { const $appMeta = doc.querySelector(`[name="${appName}/config/environment"]`);