Skip to content

Commit 7f0b1b0

Browse files
Desktop: Fixes #14979: Fixed copying and cutting table columns in Rich Text editor
1 parent cbdb3f1 commit 7f0b1b0

3 files changed

Lines changed: 68 additions & 21 deletions

File tree

packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ let markupToHtml_ = new MarkupToHtml();
8383
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
8484
function stripMarkup(markupLanguage: number, markup: string, options: any = null) {
8585
if (!markupToHtml_) markupToHtml_ = new MarkupToHtml();
86-
return markupToHtml_.stripMarkup(markupLanguage, markup, options);
86+
return markupToHtml_.stripMarkup(markupLanguage, markup, options);
8787
}
8888

8989
interface LastOnChangeEventInfo {
@@ -97,17 +97,17 @@ let dispatchDidUpdateIID_: any = null;
9797
let changeId_ = 1;
9898

9999
const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
100-
const [editorContainer, setEditorContainer] = useState<HTMLDivElement|null>(null);
100+
const [editorContainer, setEditorContainer] = useState<HTMLDivElement | null>(null);
101101
const editorContainerDom = useDocument(editorContainer);
102-
const [editor, setEditor] = useState<Editor|null>(null);
102+
const [editor, setEditor] = useState<Editor | null>(null);
103103
const [scriptLoaded, setScriptLoaded] = useState(false);
104104
const [editorReady, setEditorReady] = useState(false);
105105
const [draggingStarted, setDraggingStarted] = useState(false);
106106

107107
const props_onMessage = useRef(null);
108108
props_onMessage.current = props.onMessage;
109109

110-
const props_onDrop = useRef<DropHandler|null>(null);
110+
const props_onDrop = useRef<DropHandler | null>(null);
111111
props_onDrop.current = props.onDrop;
112112

113113
const markupToHtml = useRef(null);
@@ -364,7 +364,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
364364
// };
365365

366366
useEffect(() => {
367-
if (!editorContainerDom) return () => {};
367+
if (!editorContainerDom) return () => { };
368368

369369
let cancelled = false;
370370

@@ -412,7 +412,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
412412
const { resetModifiedTitles: resetLinkTooltips } = useLinkTooltips(editor);
413413

414414
useEffect(() => {
415-
if (!editorContainerDom) return () => {};
415+
if (!editorContainerDom) return () => { };
416416
const theme = themeStyle(props.themeId);
417417
const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor;
418418

@@ -1094,11 +1094,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
10941094
const noteChangeTimeRef = useRef(Date.now());
10951095
const lastNoteIdRef = useRef(props.noteId);
10961096
useEffect(() => {
1097-
if (!editor) return () => {};
1097+
if (!editor) return () => { };
10981098

10991099
if (resourcesStatus(props.resourceInfos) !== 'ready') {
11001100
editor.setContent('');
1101-
return () => {};
1101+
return () => { };
11021102
}
11031103

11041104
let cancelled = false;
@@ -1201,7 +1201,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
12011201
}, [editor, props.noteId, props.themeId, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
12021202

12031203
useEffect(() => {
1204-
if (!editor) return () => {};
1204+
if (!editor) return () => { };
12051205

12061206
editor.getDoc().addEventListener('click', onEditorContentClick);
12071207
return () => {
@@ -1213,7 +1213,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
12131213
// overlay over the editor, which makes it a valid drop target. This in
12141214
// turn makes NoteEditor get the drop event and dispatch it.
12151215
useEffect(() => {
1216-
if (!editor) return () => {};
1216+
if (!editor) return () => { };
12171217

12181218
function onDragStart() {
12191219
setDraggingStarted(true);
@@ -1295,7 +1295,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
12951295
const onChangeHandlerTimeoutRef = useRef<any>(null);
12961296

12971297
useEffect(() => {
1298-
if (!editor) return () => {};
1298+
if (!editor) return () => { };
12991299

13001300
function onChangeHandler() {
13011301
// First this component notifies the parent that a change is going to happen.
@@ -1454,12 +1454,19 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
14541454
if (pastedHtml) { // Handles HTML
14551455
logger.info('onPaste: pasting as HTML');
14561456

1457+
const inTable = !!editor.dom.getParent(editor.selection.getNode(), 'table');
1458+
14571459
const modifiedHtml = await processPastedHtml(
14581460
pastedHtml,
1459-
prop_htmlToMarkdownRef.current,
1460-
markupToHtml.current,
1461+
inTable ? null : prop_htmlToMarkdownRef.current,
1462+
inTable ? null : markupToHtml.current,
14611463
);
1462-
editor.insertContent(modifiedHtml);
1464+
1465+
if (inTable && pastedHtml.toLowerCase().indexOf('<table') >= 0) {
1466+
editor.execCommand('mceInsertClipboardContent', false, { html: modifiedHtml });
1467+
} else {
1468+
editor.insertContent(modifiedHtml);
1469+
}
14631470
} else { // Handles plain text
14641471
logger.info('onPaste: pasting as text');
14651472
pasteAsPlainText(pastedText);
@@ -1470,6 +1477,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
14701477

14711478
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
14721479
async function onCopy(event: any) {
1480+
const selectedCells = editor.dom.select('td[data-mce-selected="1"], th[data-mce-selected="1"]');
1481+
if (selectedCells.length > 1 || editor.dom.select('tr[data-mce-selected="1"]').length > 0) {
1482+
setTimeout(() => {
1483+
const clipboardHtml = clipboard.readHTML();
1484+
if (clipboardHtml) copyHtmlToClipboard(clipboardHtml);
1485+
}, 50);
1486+
return;
1487+
}
1488+
14731489
const copiedContent = editor.selection.getContent();
14741490
if (!copiedContent) return;
14751491
copyHtmlToClipboard(copiedContent);
@@ -1478,6 +1494,16 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
14781494

14791495
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
14801496
async function onCut(event: any) {
1497+
const selectedCells = editor.dom.select('td[data-mce-selected="1"], th[data-mce-selected="1"]');
1498+
if (selectedCells.length > 1 || editor.dom.select('tr[data-mce-selected="1"]').length > 0) {
1499+
setTimeout(() => {
1500+
const clipboardHtml = clipboard.readHTML();
1501+
if (clipboardHtml) copyHtmlToClipboard(clipboardHtml);
1502+
onChangeHandler();
1503+
}, 50);
1504+
return;
1505+
}
1506+
14811507
event.preventDefault();
14821508
const selectedContent = editor.selection.getContent();
14831509
if (!selectedContent) return;
@@ -1683,7 +1709,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
16831709
{renderDisabledOverlay()}
16841710
{renderLeftExtraToolbarButtons()}
16851711
{renderRightExtraToolbarButtons()}
1686-
<div style={{ width: '100%', height: '100%' }} id={containerId} ref={setEditorContainer}/>
1712+
<div style={{ width: '100%', height: '100%' }} id={containerId} ref={setEditorContainer} />
16871713
</div>
16881714
);
16891715
};

packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,26 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
6969
linkToCopy: linkUrl,
7070
linkToOpen: linkUrl,
7171
textToCopy: null,
72-
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
72+
htmlToCopy: (() => {
73+
if (!editor.selection) return '';
74+
const selectedCells = editor.dom.select('td[data-mce-selected="1"], th[data-mce-selected="1"]');
75+
if (selectedCells.length > 1 || editor.dom.select('tr[data-mce-selected="1"]').length > 0) {
76+
return 'table-selected';
77+
}
78+
return editor.selection.getContent();
79+
})(),
7380
insertContent: (content: string) => {
7481
editor.insertContent(content);
7582
},
7683
isReadOnly: false,
77-
fireEditorEvent: (event: TinyMceEditorEvents) => {
78-
editor.fire(event);
84+
fireEditorEvent: (event: TinyMceEditorEvents|string) => {
85+
if (event === 'execCommandCopy') {
86+
editor.getDoc().execCommand('copy');
87+
} else if (event === 'execCommandCut') {
88+
editor.getDoc().execCommand('cut');
89+
} else {
90+
editor.fire(event as TinyMceEditorEvents);
91+
}
7992
},
8093
htmlToMd,
8194
mdToHtml,

packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,23 @@ export function menuItems(dispatch: Function): ContextMenuItems {
260260
cut: {
261261
label: _('Cut'),
262262
onAction: async (options: ContextMenuOptions) => {
263-
handleCopyToClipboard(options);
264-
options.insertContent('');
263+
if (options.htmlToCopy === 'table-selected') {
264+
options.fireEditorEvent('execCommandCut');
265+
} else {
266+
handleCopyToClipboard(options);
267+
options.insertContent('');
268+
}
265269
},
266270
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
267271
},
268272
copy: {
269273
label: _('Copy'),
270274
onAction: async (options: ContextMenuOptions) => {
271-
handleCopyToClipboard(options);
275+
if (options.htmlToCopy === 'table-selected') {
276+
options.fireEditorEvent('execCommandCopy');
277+
} else {
278+
handleCopyToClipboard(options);
279+
}
272280
},
273281
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType !== ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
274282
},

0 commit comments

Comments
 (0)