Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OGUI-1606] Add copy-to-clipboard component #2714

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions Framework/Frontend/js/src/components/CopyToClipboardComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import { StatefulComponent } from './StatefulComponent.js';
import { iconCheck, iconLinkIntact } from '../icons.js';
import { h } from '../renderer.js';

/**
* Represents a component that allows copying text to the clipboard.
*/
export class CopyToClipboardComponent extends StatefulComponent {
/**
* Constructs a new CopyToClipboardComponent.
*/
constructor() {
super();
this._successStateTimeout = null;
}

/**
* Copies the specified text to the clipboard.
*
* @param {string} clipboardTargetValue The text to be copied to the clipboard.
* @returns {void}
*/
copyToClipboard(clipboardTargetValue) {
navigator.clipboard.writeText(clipboardTargetValue);
this._successStateTimeout = setTimeout(() => {
this._successStateTimeout = null;
this.notify();
}, 2000);
this.notify();
}

/**
* Checks the availability of the clipboard and provides a message if it is not accessible.
*
* @throws {Error} Throws an error with a descriptive message if the clipboard is not available.
* @returns {void}
*/
checkClipboardAvailability() {
if (!this.isContextSecure()) {
throw new Error('Clipboard not available in a non-secure context.');
}

if (!this.isClipboardSupported()) {
throw new Error('Clipboard API is not supported in this browser.');
}

if (this.isWindowEmbedded()) {
throw new Error('Clipboard access is restricted in iframes.');
}
}

/**
* Checks if context is secure (HTTPS)
*
* @returns {boolean} Returns `true` if context is secure
*/
isContextSecure() {
return window.isSecureContext;
}

/**
* Checks if the clipboard API is available in the user's browser.
*
* @returns {boolean} Returns `true` if it is available
*/
isClipboardSupported() {
return Boolean(navigator.clipboard);
}

/**
* Check if the window is embeded in a frame.
*
* @returns {boolean} Returns `true` if it is embeded
*/
isWindowEmbedded() {
return window !== window.parent;
}

/**
* Renders the button that allows copying text to the clipboard.
*
* @param {vnode} vnode The virtual DOM node containing the attrs and children.
* @returns {vnode} The copyToClipboard button component
*/
view(vnode) {
const { attrs, children } = vnode;
const { value: clipboardTargetValue = '', id } = attrs;
let available = true;
let message = '';

try {
this.checkClipboardAvailability();
} catch ({ message: errorMessage }) {
available = false;
message = errorMessage;
}

const defaultContent = [iconLinkIntact(), children];
const successContent = [iconCheck(), h('', 'Copied!')];

return h(
'button.btn.btn-primary',
{
id: `copy-${id}`,
onclick: () => this.copyToClipboard(clipboardTargetValue),
disabled: !available,
title: message || null,
},
h('div.flex-row.g1', this._successStateTimeout ? successContent : defaultContent),
);
}
}
47 changes: 47 additions & 0 deletions Framework/Frontend/js/src/components/StatefulComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
import Observable from '../Observable.js';

// No static property in es2020, using module encapsulation instead
let _renderer = null;

/**
* Specific component that can triggers re-render on its own when its property changes
*
* Some components do not really require models, for example dropdowns. However, we need a way for these components to trigger re-render
* without having to pass a model all the way along to it. This is the purpose of this component: calling their notify function will trigger
* a re-render
* At the application startup, a renderer should be provided in order for the component to trigger re-rendering (use the function
* `useRenderer` once, passing the global model to it)
*/
export class StatefulComponent extends Observable {
/**
* Set the current renderer that the stateful components should notify when their state changed
*
* @param {{notify: function}} renderer the renderer to use
* @return {void}
*/
static useRenderer(renderer) {
_renderer = renderer;
}

/**
* @inheritDoc
*/
notify() {
super.notify();
if (_renderer) {
_renderer.notify();
}
}
}
4 changes: 4 additions & 0 deletions Framework/Frontend/js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export { default as WebSocketClient } from './WebSocketClient.js';
export { default as Loader } from './Loader.js';
export { default as BrowserStorage } from './BrowserStorage.js';

// Reusable components
export { StatefulComponent } from './components/StatefulComponent.js';
export { CopyToClipboardComponent } from './components/CopyToClipboardComponent.js';

// All icons helpers, namespaced with prefix 'icon*'
export * from './icons.js';

Expand Down
Loading