mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[PM-2043] Fix additional space and characters copied to clipboard (#5312)
* Change appSelectCopy to accept a dynamic input on what to copy * Renamed select-copy directive to copy-text directive to be more accurate with the new behaviour Signed-off-by: Andre Rosado <arosado@bitwarden.com> * Moved CopyTextDirective on jslib module to be in alphabetic ordering --------- Signed-off-by: Andre Rosado <arosado@bitwarden.com> Co-authored-by: Andre Rosado <arosado@bitwarden.com>
This commit is contained in:
@@ -19,7 +19,11 @@
|
|||||||
{{ "passwordGeneratorPolicyInEffect" | i18n }}
|
{{ "passwordGeneratorPolicyInEffect" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<div class="generated-block" *ngIf="type === 'password'">
|
<div class="generated-block" *ngIf="type === 'password'">
|
||||||
<div class="generated-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
|
<div
|
||||||
|
class="generated-wrapper"
|
||||||
|
[innerHTML]="password | colorPassword"
|
||||||
|
[appCopyText]="password"
|
||||||
|
></div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -41,7 +45,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="generated-block" *ngIf="type === 'username'">
|
<div class="generated-block" *ngIf="type === 'username'">
|
||||||
<div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
|
<div
|
||||||
|
class="generated-wrapper"
|
||||||
|
[innerHTML]="username | colorPassword"
|
||||||
|
[appCopyText]="username"
|
||||||
|
></div>
|
||||||
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
|
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="row-main-content">
|
<div class="row-main-content">
|
||||||
<div
|
<div
|
||||||
class="monospaced password-wrapper"
|
class="monospaced password-wrapper"
|
||||||
appSelectCopy
|
[appCopyText]="h.password"
|
||||||
[innerHTML]="h.password | colorPassword"
|
[innerHTML]="h.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
<span class="detail">{{ h.date | date : "medium" }}</span>
|
<span class="detail">{{ h.date | date : "medium" }}</span>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<div
|
<div
|
||||||
*ngIf="showPassword && !showPasswordCount"
|
*ngIf="showPassword && !showPasswordCount"
|
||||||
class="monospaced password-wrapper"
|
class="monospaced password-wrapper"
|
||||||
appSelectCopy
|
[appCopyText]="cipher.login.password"
|
||||||
[innerHTML]="cipher.login.password | colorPassword"
|
[innerHTML]="cipher.login.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
{{ "passwordGeneratorPolicyInEffect" | i18n }}
|
{{ "passwordGeneratorPolicyInEffect" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<div class="generated-block" *ngIf="type === 'password'">
|
<div class="generated-block" *ngIf="type === 'password'">
|
||||||
<div class="generated-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
|
<div
|
||||||
|
class="generated-wrapper"
|
||||||
|
[innerHTML]="password | colorPassword"
|
||||||
|
[appCopyText]="password"
|
||||||
|
></div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -35,7 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="generated-block" *ngIf="type === 'username'">
|
<div class="generated-block" *ngIf="type === 'username'">
|
||||||
<div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
|
<div
|
||||||
|
class="generated-wrapper"
|
||||||
|
[innerHTML]="username | colorPassword"
|
||||||
|
[appCopyText]="username"
|
||||||
|
></div>
|
||||||
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
|
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<div
|
<div
|
||||||
class="password-wrapper monospaced"
|
class="password-wrapper monospaced"
|
||||||
appSelectCopy
|
[appCopyText]="h.password"
|
||||||
[innerHTML]="h.password | colorPassword"
|
[innerHTML]="h.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
<span class="detail">{{ h.date | date : "medium" }}</span>
|
<span class="detail">{{ h.date | date : "medium" }}</span>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<div
|
<div
|
||||||
*ngIf="showPassword && !showPasswordCount"
|
*ngIf="showPassword && !showPasswordCount"
|
||||||
class="monospaced password-wrapper"
|
class="monospaced password-wrapper"
|
||||||
appSelectCopy
|
[appCopyText]="cipher.login.password"
|
||||||
[innerHTML]="cipher.login.password | colorPassword"
|
[innerHTML]="cipher.login.password | colorPassword"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<bit-color-password
|
<bit-color-password
|
||||||
[password]="type === 'password' ? password : username"
|
[password]="type === 'password' ? password : username"
|
||||||
appSelectCopy
|
[appCopyText]="type === 'password' ? password : username"
|
||||||
></bit-color-password>
|
></bit-color-password>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<bit-color-password
|
<bit-color-password
|
||||||
[password]="h.password"
|
[password]="h.password"
|
||||||
class="tw-block tw-font-mono"
|
class="tw-block tw-font-mono"
|
||||||
appSelectCopy
|
[appCopyText]="h.password"
|
||||||
></bit-color-password>
|
></bit-color-password>
|
||||||
<small bitTypography="body2" class="tw-text-muted">
|
<small bitTypography="body2" class="tw-text-muted">
|
||||||
{{ h.date | date : "medium" }}
|
{{ h.date | date : "medium" }}
|
||||||
|
|||||||
20
libs/angular/src/directives/copy-text.directive.ts
Normal file
20
libs/angular/src/directives/copy-text.directive.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appCopyText]",
|
||||||
|
})
|
||||||
|
export class CopyTextDirective {
|
||||||
|
constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) {}
|
||||||
|
|
||||||
|
@Input("appCopyText") copyText: string;
|
||||||
|
|
||||||
|
@HostListener("copy") onCopy() {
|
||||||
|
if (window == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.copyToClipboard(this.copyText, { window: window });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Directive, ElementRef, HostListener } from "@angular/core";
|
|
||||||
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: "[appSelectCopy]",
|
|
||||||
})
|
|
||||||
export class SelectCopyDirective {
|
|
||||||
constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) {}
|
|
||||||
|
|
||||||
@HostListener("copy") onCopy() {
|
|
||||||
if (window == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let copyText = "";
|
|
||||||
const selection = window.getSelection();
|
|
||||||
for (let i = 0; i < selection.rangeCount; i++) {
|
|
||||||
const range = selection.getRangeAt(i);
|
|
||||||
const text = range.toString();
|
|
||||||
|
|
||||||
// The selection should only contain one line of text. In some cases however, the
|
|
||||||
// selection contains newlines and space characters from the indentation of following
|
|
||||||
// sibling nodes. To avoid copying passwords containing trailing newlines and spaces
|
|
||||||
// that aren't part of the password, the selection has to be trimmed.
|
|
||||||
let stringEndPos = text.length;
|
|
||||||
const newLinePos = text.search(/(?:\r\n|\r|\n)/);
|
|
||||||
if (newLinePos > -1) {
|
|
||||||
const otherPart = text.substr(newLinePos).trim();
|
|
||||||
if (otherPart === "") {
|
|
||||||
stringEndPos = newLinePos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copyText += text.substring(0, stringEndPos);
|
|
||||||
}
|
|
||||||
this.platformUtilsService.copyToClipboard(copyText, { window: window });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,13 +10,13 @@ import { ApiActionDirective } from "./directives/api-action.directive";
|
|||||||
import { AutofocusDirective } from "./directives/autofocus.directive";
|
import { AutofocusDirective } from "./directives/autofocus.directive";
|
||||||
import { BoxRowDirective } from "./directives/box-row.directive";
|
import { BoxRowDirective } from "./directives/box-row.directive";
|
||||||
import { CopyClickDirective } from "./directives/copy-click.directive";
|
import { CopyClickDirective } from "./directives/copy-click.directive";
|
||||||
|
import { CopyTextDirective } from "./directives/copy-text.directive";
|
||||||
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
|
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
|
||||||
import { IfFeatureDirective } from "./directives/if-feature.directive";
|
import { IfFeatureDirective } from "./directives/if-feature.directive";
|
||||||
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
|
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
|
||||||
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
|
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
|
||||||
import { LaunchClickDirective } from "./directives/launch-click.directive";
|
import { LaunchClickDirective } from "./directives/launch-click.directive";
|
||||||
import { NotPremiumDirective } from "./directives/not-premium.directive";
|
import { NotPremiumDirective } from "./directives/not-premium.directive";
|
||||||
import { SelectCopyDirective } from "./directives/select-copy.directive";
|
|
||||||
import { StopClickDirective } from "./directives/stop-click.directive";
|
import { StopClickDirective } from "./directives/stop-click.directive";
|
||||||
import { StopPropDirective } from "./directives/stop-prop.directive";
|
import { StopPropDirective } from "./directives/stop-prop.directive";
|
||||||
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
||||||
@@ -50,6 +50,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
|||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
CalloutComponent,
|
CalloutComponent,
|
||||||
|
CopyTextDirective,
|
||||||
CreditCardNumberPipe,
|
CreditCardNumberPipe,
|
||||||
EllipsisPipe,
|
EllipsisPipe,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
@@ -61,7 +62,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
|||||||
NotPremiumDirective,
|
NotPremiumDirective,
|
||||||
SearchCiphersPipe,
|
SearchCiphersPipe,
|
||||||
SearchPipe,
|
SearchPipe,
|
||||||
SelectCopyDirective,
|
|
||||||
StopClickDirective,
|
StopClickDirective,
|
||||||
StopPropDirective,
|
StopPropDirective,
|
||||||
TrueFalseValueDirective,
|
TrueFalseValueDirective,
|
||||||
@@ -81,6 +81,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
|||||||
BitwardenToastModule,
|
BitwardenToastModule,
|
||||||
BoxRowDirective,
|
BoxRowDirective,
|
||||||
CalloutComponent,
|
CalloutComponent,
|
||||||
|
CopyTextDirective,
|
||||||
CreditCardNumberPipe,
|
CreditCardNumberPipe,
|
||||||
EllipsisPipe,
|
EllipsisPipe,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
@@ -92,7 +93,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
|||||||
NotPremiumDirective,
|
NotPremiumDirective,
|
||||||
SearchCiphersPipe,
|
SearchCiphersPipe,
|
||||||
SearchPipe,
|
SearchPipe,
|
||||||
SelectCopyDirective,
|
|
||||||
StopClickDirective,
|
StopClickDirective,
|
||||||
StopPropDirective,
|
StopPropDirective,
|
||||||
TrueFalseValueDirective,
|
TrueFalseValueDirective,
|
||||||
|
|||||||
Reference in New Issue
Block a user