mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
[signalapp/Signal-Desktop#7512] Improve copy-paste formatting inheritance
Co-authored-by: Brian Harder <briankharder@gmail.com>
This commit is contained in:
@@ -80,33 +80,41 @@ export class SignalClipboard {
|
||||
}
|
||||
|
||||
const { ops } = this.quill.getContents(selection.index, selection.length);
|
||||
// Only enable formatting on the pasted text if the entire selection has it enabled!
|
||||
const formats =
|
||||
selection.length === 0
|
||||
? this.quill.getFormat(selection.index)
|
||||
: {
|
||||
[QuillFormattingStyle.bold]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.bold
|
||||
),
|
||||
[QuillFormattingStyle.italic]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.italic
|
||||
),
|
||||
[QuillFormattingStyle.monospace]:
|
||||
FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.monospace
|
||||
),
|
||||
[QuillFormattingStyle.spoiler]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.spoiler
|
||||
),
|
||||
[QuillFormattingStyle.strike]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.strike
|
||||
),
|
||||
};
|
||||
|
||||
// Check if we're selecting all content
|
||||
const totalLength = this.quill.getLength();
|
||||
const isSelectingAll = selection.length >= totalLength - 1;
|
||||
|
||||
let formats: Record<string, unknown>;
|
||||
if (selection.length === 0) {
|
||||
formats = this.quill.getFormat(selection.index);
|
||||
} else if (isSelectingAll) {
|
||||
// No formatting for select-all
|
||||
formats = {};
|
||||
} else {
|
||||
formats = {
|
||||
[QuillFormattingStyle.bold]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.bold
|
||||
),
|
||||
[QuillFormattingStyle.italic]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.italic
|
||||
),
|
||||
[QuillFormattingStyle.monospace]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.monospace
|
||||
),
|
||||
[QuillFormattingStyle.spoiler]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.spoiler
|
||||
),
|
||||
[QuillFormattingStyle.strike]: FormattingMenu.isStyleEnabledForOps(
|
||||
ops,
|
||||
QuillFormattingStyle.strike
|
||||
),
|
||||
};
|
||||
}
|
||||
const clipboardDelta = signal
|
||||
? clipboard.convert({ html: signal }, formats)
|
||||
: new Delta(insertEmojiOps(clipboard.convert({ text }, formats).ops, {}));
|
||||
|
||||
190
ts/test-electron/quill/signal-clipboard_test.dom.ts
Normal file
190
ts/test-electron/quill/signal-clipboard_test.dom.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Delta } from '@signalapp/quill-cjs';
|
||||
import type Quill from '@signalapp/quill-cjs';
|
||||
|
||||
import { SignalClipboard } from '../../quill/signal-clipboard/index.dom.js';
|
||||
import { QuillFormattingStyle } from '../../quill/formatting/menu.dom.js';
|
||||
|
||||
class MockQuill {
|
||||
public root: HTMLElement;
|
||||
public clipboard: {
|
||||
convert: (data: unknown, formats: Record<string, unknown>) => Delta;
|
||||
};
|
||||
public selection: {
|
||||
getRange: () => Array<unknown> | [null];
|
||||
update: (mode: string) => void;
|
||||
};
|
||||
public getContents: (
|
||||
index: number,
|
||||
length: number
|
||||
) => { ops: Array<unknown> };
|
||||
public getSelection: () => { index: number; length: number } | null;
|
||||
public getLength: () => number;
|
||||
public getFormat: (index: number) => Record<string, unknown>;
|
||||
public updateContents: (delta: Delta, source: string) => void;
|
||||
public setSelection: (index: number, length: number, mode: string) => void;
|
||||
public scrollSelectionIntoView: () => void;
|
||||
public focus: () => void;
|
||||
|
||||
constructor() {
|
||||
this.root = document.createElement('div');
|
||||
this.clipboard = {
|
||||
convert: (_data: unknown, formats: Record<string, unknown>) => {
|
||||
// Mock clipboard conversion - returns delta
|
||||
const text = 'test';
|
||||
return new Delta([{ insert: text, attributes: formats }]);
|
||||
},
|
||||
};
|
||||
this.selection = {
|
||||
getRange: () => [null],
|
||||
update: () => {
|
||||
// Placeholder for linter
|
||||
},
|
||||
};
|
||||
this.getContents = (_index: number, _length: number) => ({ ops: [] });
|
||||
this.getSelection = () => ({ index: 0, length: 0 });
|
||||
this.getLength = () => 1;
|
||||
this.getFormat = () => ({});
|
||||
this.updateContents = () => {
|
||||
// Placeholder for linter
|
||||
};
|
||||
this.setSelection = () => {
|
||||
// Placeholder for linter
|
||||
};
|
||||
this.scrollSelectionIntoView = () => {
|
||||
// Placeholder for linter
|
||||
};
|
||||
this.focus = () => {
|
||||
// Placeholder for linter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createMockClipboardEvent(
|
||||
textData: string | null = null,
|
||||
signalData: string | null = null
|
||||
): ClipboardEvent {
|
||||
const event = new Event('paste') as ClipboardEvent;
|
||||
Object.defineProperty(event, 'clipboardData', {
|
||||
value: {
|
||||
getData: (format: string) => {
|
||||
if (format === 'text/plain') {
|
||||
return textData || '';
|
||||
}
|
||||
if (format === 'text/signal') {
|
||||
return signalData || '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
files: null,
|
||||
} as unknown as DataTransfer,
|
||||
writable: false,
|
||||
});
|
||||
return event;
|
||||
}
|
||||
|
||||
function createMockQuillWithContent(
|
||||
content: string,
|
||||
hasStrike: boolean = false
|
||||
): MockQuill {
|
||||
const mockQuill = new MockQuill();
|
||||
|
||||
mockQuill.getContents = () => ({
|
||||
ops: [
|
||||
{
|
||||
insert: content,
|
||||
attributes: hasStrike ? { [QuillFormattingStyle.strike]: true } : {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockQuill.getLength = () => content.length + 1;
|
||||
|
||||
return mockQuill;
|
||||
}
|
||||
|
||||
describe('SignalClipboard', () => {
|
||||
let mockQuill: MockQuill;
|
||||
let clipboard: SignalClipboard;
|
||||
|
||||
beforeEach(() => {
|
||||
mockQuill = new MockQuill();
|
||||
clipboard = new SignalClipboard(mockQuill as unknown as Quill, {
|
||||
isDisabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('onCapturePaste', () => {
|
||||
describe('when pasting plain text', () => {
|
||||
it('should not inherit strikethrough formatting from selected text', () => {
|
||||
const content = 'Hello world';
|
||||
mockQuill = createMockQuillWithContent(content, true);
|
||||
clipboard = new SignalClipboard(mockQuill as unknown as Quill, {
|
||||
isDisabled: false,
|
||||
});
|
||||
|
||||
// Select all
|
||||
mockQuill.getSelection = () => ({ index: 0, length: content.length });
|
||||
|
||||
// Conversion to delta
|
||||
let capturedFormats: Record<string, unknown> | null = null;
|
||||
mockQuill.clipboard.convert = (
|
||||
_data: unknown,
|
||||
formats: Record<string, unknown>
|
||||
) => {
|
||||
capturedFormats = formats;
|
||||
return new Delta([{ insert: 'test', attributes: formats }]);
|
||||
};
|
||||
|
||||
// Paste
|
||||
const pasteEvent = createMockClipboardEvent('New text', null);
|
||||
clipboard.onCapturePaste(pasteEvent);
|
||||
|
||||
// Assert no formatting
|
||||
assert.deepEqual(capturedFormats, {});
|
||||
});
|
||||
|
||||
it('should not inherit any formatting from selected text', () => {
|
||||
const content = 'Hello world';
|
||||
mockQuill = createMockQuillWithContent(content, false);
|
||||
mockQuill.getContents = () => ({
|
||||
ops: [
|
||||
{
|
||||
insert: content,
|
||||
attributes: {
|
||||
[QuillFormattingStyle.bold]: true,
|
||||
[QuillFormattingStyle.italic]: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
clipboard = new SignalClipboard(mockQuill as unknown as Quill, {
|
||||
isDisabled: false,
|
||||
});
|
||||
|
||||
// Select all content
|
||||
mockQuill.getSelection = () => ({ index: 0, length: content.length });
|
||||
|
||||
// Conversion to delta
|
||||
let capturedFormats: Record<string, unknown> | null = null;
|
||||
mockQuill.clipboard.convert = (
|
||||
_data: unknown,
|
||||
formats: Record<string, unknown>
|
||||
) => {
|
||||
capturedFormats = formats;
|
||||
return new Delta([{ insert: 'test', attributes: formats }]);
|
||||
};
|
||||
|
||||
// Paste
|
||||
const pasteEvent = createMockClipboardEvent('New text', null);
|
||||
clipboard.onCapturePaste(pasteEvent);
|
||||
|
||||
// Assert no formatting
|
||||
assert.deepEqual(capturedFormats, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user