mirror of https://github.com/vector-im/riot-web
consolidate and extract copyPlaintext, copyNode and selectText
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
parent
36fea4d487
commit
89bc3bdd5b
|
@ -25,6 +25,7 @@ import { _t } from '../../../../languageHandler';
|
||||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
import SettingsStore from '../../../../settings/SettingsStore';
|
import SettingsStore from '../../../../settings/SettingsStore';
|
||||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||||
|
import {copyNode} from "../../../../utils/strings";
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -37,16 +38,6 @@ const PHASE_OPTOUT_CONFIRM = 6;
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
||||||
|
|
||||||
// XXX: copied from ShareDialog: factor out into utils
|
|
||||||
function selectText(target) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(target);
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating an e2e key backup
|
* Walks the user through the process of creating an e2e key backup
|
||||||
* on the server.
|
* on the server.
|
||||||
|
@ -101,8 +92,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCopyClick = () => {
|
_onCopyClick = () => {
|
||||||
selectText(this._recoveryKeyNode);
|
const successful = copyNode(this._recoveryKeyNode);
|
||||||
const successful = document.execCommand('copy');
|
|
||||||
if (successful) {
|
if (successful) {
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: true,
|
copied: true,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import FileSaver from 'file-saver';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
|
||||||
|
import {copyNode} from "../../../../utils/strings";
|
||||||
|
|
||||||
const PHASE_LOADING = 0;
|
const PHASE_LOADING = 0;
|
||||||
const PHASE_MIGRATE = 1;
|
const PHASE_MIGRATE = 1;
|
||||||
|
@ -38,16 +39,6 @@ const PHASE_CONFIRM_SKIP = 8;
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
||||||
|
|
||||||
// XXX: copied from ShareDialog: factor out into utils
|
|
||||||
function selectText(target) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(target);
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating a passphrase to guard Secure
|
* Walks the user through the process of creating a passphrase to guard Secure
|
||||||
* Secret Storage in account data.
|
* Secret Storage in account data.
|
||||||
|
@ -169,8 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCopyClick = () => {
|
_onCopyClick = () => {
|
||||||
selectText(this._recoveryKeyNode);
|
const successful = copyNode(this._recoveryKeyNode);
|
||||||
const successful = document.execCommand('copy');
|
|
||||||
if (successful) {
|
if (successful) {
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: true,
|
copied: true,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import QRCode from 'qrcode-react';
|
||||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||||
import * as ContextMenu from "../../structures/ContextMenu";
|
import * as ContextMenu from "../../structures/ContextMenu";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import {toRightOf} from "../../structures/ContextMenu";
|
||||||
|
import {copyPlaintext, selectText} from "../../../utils/strings";
|
||||||
|
|
||||||
const socials = [
|
const socials = [
|
||||||
{
|
{
|
||||||
|
@ -81,45 +82,26 @@ export default class ShareDialog extends React.Component {
|
||||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._link = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
static _selectText(target) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(target);
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static onLinkClick(e) {
|
static onLinkClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const {target} = e;
|
selectText(e.target);
|
||||||
ShareDialog._selectText(target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyClick(e) {
|
async onCopyClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const target = e.target; // copy target before we go async and React throws it away
|
||||||
|
|
||||||
ShareDialog._selectText(this._link.current);
|
const successful = await copyPlaintext(this.getUrl());
|
||||||
|
const buttonRect = target.getBoundingClientRect();
|
||||||
let successful;
|
|
||||||
try {
|
|
||||||
successful = document.execCommand('copy');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy: ', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonRect = e.target.getBoundingClientRect();
|
|
||||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
...toRightOf(buttonRect, 2),
|
...toRightOf(buttonRect, 2),
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
});
|
||||||
// Drop a reference to this close handler for componentWillUnmount
|
// Drop a reference to this close handler for componentWillUnmount
|
||||||
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLinkSpecificEventCheckboxClick() {
|
onLinkSpecificEventCheckboxClick() {
|
||||||
|
@ -134,10 +116,32 @@ export default class ShareDialog extends React.Component {
|
||||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
getUrl() {
|
||||||
let title;
|
|
||||||
let matrixToUrl;
|
let matrixToUrl;
|
||||||
|
|
||||||
|
if (this.props.target instanceof Room) {
|
||||||
|
if (this.state.linkSpecificEvent) {
|
||||||
|
const events = this.props.target.getLiveTimeline().getEvents();
|
||||||
|
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||||
|
} else {
|
||||||
|
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||||
|
}
|
||||||
|
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||||
|
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||||
|
} else if (this.props.target instanceof Group) {
|
||||||
|
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||||
|
} else if (this.props.target instanceof MatrixEvent) {
|
||||||
|
if (this.state.linkSpecificEvent) {
|
||||||
|
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||||
|
} else {
|
||||||
|
matrixToUrl = this.props.permalinkCreator.forRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matrixToUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let title;
|
||||||
let checkbox;
|
let checkbox;
|
||||||
|
|
||||||
if (this.props.target instanceof Room) {
|
if (this.props.target instanceof Room) {
|
||||||
|
@ -155,18 +159,10 @@ export default class ShareDialog extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.linkSpecificEvent) {
|
|
||||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
|
||||||
} else {
|
|
||||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
|
||||||
}
|
|
||||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||||
title = _t('Share User');
|
title = _t('Share User');
|
||||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
|
||||||
} else if (this.props.target instanceof Group) {
|
} else if (this.props.target instanceof Group) {
|
||||||
title = _t('Share Community');
|
title = _t('Share Community');
|
||||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
|
||||||
} else if (this.props.target instanceof MatrixEvent) {
|
} else if (this.props.target instanceof MatrixEvent) {
|
||||||
title = _t('Share Room Message');
|
title = _t('Share Room Message');
|
||||||
checkbox = <div>
|
checkbox = <div>
|
||||||
|
@ -178,14 +174,9 @@ export default class ShareDialog extends React.Component {
|
||||||
{ _t('Link to selected message') }
|
{ _t('Link to selected message') }
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
if (this.state.linkSpecificEvent) {
|
|
||||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
|
||||||
} else {
|
|
||||||
matrixToUrl = this.props.permalinkCreator.forRoom();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matrixToUrl = this.getUrl();
|
||||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
@ -196,8 +187,7 @@ export default class ShareDialog extends React.Component {
|
||||||
>
|
>
|
||||||
<div className="mx_ShareDialog_content">
|
<div className="mx_ShareDialog_content">
|
||||||
<div className="mx_ShareDialog_matrixto">
|
<div className="mx_ShareDialog_matrixto">
|
||||||
<a ref={this._link}
|
<a href={matrixToUrl}
|
||||||
href={matrixToUrl}
|
|
||||||
onClick={ShareDialog.onLinkClick}
|
onClick={ShareDialog.onLinkClick}
|
||||||
className="mx_ShareDialog_matrixto_link"
|
className="mx_ShareDialog_matrixto_link"
|
||||||
>
|
>
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import {toRightOf} from "../../structures/ContextMenu";
|
||||||
|
import {copyPlaintext} from "../../../utils/strings";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'TextualBody',
|
displayName: 'TextualBody',
|
||||||
|
@ -69,23 +70,6 @@ export default createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
copyToClipboard: function(text) {
|
|
||||||
const textArea = document.createElement("textarea");
|
|
||||||
textArea.value = text;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
let successful = false;
|
|
||||||
try {
|
|
||||||
successful = document.execCommand('copy');
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Unable to copy');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
return successful;
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||||
UNSAFE_componentWillMount: function() {
|
UNSAFE_componentWillMount: function() {
|
||||||
this._content = createRef();
|
this._content = createRef();
|
||||||
|
@ -277,17 +261,17 @@ export default createReactClass({
|
||||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||||
const button = document.createElement("span");
|
const button = document.createElement("span");
|
||||||
button.className = "mx_EventTile_copyButton";
|
button.className = "mx_EventTile_copyButton";
|
||||||
button.onclick = (e) => {
|
button.onclick = async () => {
|
||||||
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
||||||
const successful = this.copyToClipboard(copyCode.textContent);
|
const successful = await copyPlaintext(copyCode.textContent);
|
||||||
|
|
||||||
const buttonRect = e.target.getBoundingClientRect();
|
const buttonRect = button.getBoundingClientRect();
|
||||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
...toRightOf(buttonRect, 2),
|
...toRightOf(buttonRect, 2),
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
});
|
||||||
e.target.onmouseleave = close;
|
button.onmouseleave = close;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy plaintext to user's clipboard
|
||||||
|
* It will overwrite user's selection range
|
||||||
|
* In certain browsers it may only work if triggered by a user action or may ask user for permissions
|
||||||
|
* Tries to use new async clipboard API if available
|
||||||
|
* @param text the plaintext to put in the user's clipboard
|
||||||
|
*/
|
||||||
|
export async function copyPlaintext(text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
|
||||||
|
// Avoid scrolling to bottom
|
||||||
|
textArea.style.top = "0";
|
||||||
|
textArea.style.left = "0";
|
||||||
|
textArea.style.position = "fixed";
|
||||||
|
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const range = document.createRange();
|
||||||
|
// range.selectNodeContents(textArea);
|
||||||
|
range.selectNode(textArea);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
const successful = document.execCommand("copy");
|
||||||
|
selection.removeAllRanges();
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return successful;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectText(target: Element) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(target);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy rich text to user's clipboard
|
||||||
|
* It will overwrite user's selection range
|
||||||
|
* In certain browsers it may only work if triggered by a user action or may ask user for permissions
|
||||||
|
* @param ref pointer to the node to copy
|
||||||
|
*/
|
||||||
|
export function copyNode(ref: Element): boolean {
|
||||||
|
selectText(ref);
|
||||||
|
return document.execCommand('copy');
|
||||||
|
}
|
Loading…
Reference in New Issue