mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-49368 : Pasting non plain text clears undo history of Textbox (#23535)
This commit is contained in:
parent
ba4dc1a91c
commit
b9cd2a9814
@ -8,9 +8,14 @@ import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
|||||||
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
||||||
|
|
||||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||||
|
import {execCommandInsertText} from 'utils/exec_commands';
|
||||||
|
|
||||||
import AdvancedCreateComment from 'components/advanced_create_comment/advanced_create_comment';
|
import AdvancedCreateComment from 'components/advanced_create_comment/advanced_create_comment';
|
||||||
import AdvanceTextEditor from '../advanced_text_editor/advanced_text_editor';
|
import AdvanceTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||||
|
|
||||||
|
jest.mock('utils/exec_commands', () => ({
|
||||||
|
execCommandInsertText: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('components/AdvancedCreateComment', () => {
|
describe('components/AdvancedCreateComment', () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
@ -1314,7 +1319,7 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
expect(scrollToBottom).toBeCalledTimes(2);
|
expect(scrollToBottom).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a pasted markdown table', () => {
|
test('should be able to format a pasted markdown table', () => {
|
||||||
const draft = emptyDraft;
|
const draft = emptyDraft;
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<AdvancedCreateComment
|
<AdvancedCreateComment
|
||||||
@ -1354,10 +1359,10 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('draft').message).toBe(markdownTable);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a pasted markdown table without headers', () => {
|
test('should be able to format a pasted markdown table without headers', () => {
|
||||||
const draft = emptyDraft;
|
const draft = emptyDraft;
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<AdvancedCreateComment
|
<AdvancedCreateComment
|
||||||
@ -1397,10 +1402,10 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('draft').message).toBe(markdownTable);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a pasted hyperlink', () => {
|
test('should be able to format a pasted hyperlink', () => {
|
||||||
const draft = emptyDraft;
|
const draft = emptyDraft;
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<AdvancedCreateComment
|
<AdvancedCreateComment
|
||||||
@ -1440,10 +1445,10 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
const markdownLink = '[link text](https://test.domain)';
|
const markdownLink = '[link text](https://test.domain)';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('draft').message).toBe(markdownLink);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a github codeblock (pasted as a table)', () => {
|
test('should be able to format a github codeblock (pasted as a table)', () => {
|
||||||
const draft = emptyDraft;
|
const draft = emptyDraft;
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<AdvancedCreateComment
|
<AdvancedCreateComment
|
||||||
@ -1486,60 +1491,7 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('draft').message).toBe(codeBlockMarkdown);
|
expect(execCommandInsertText).toHaveBeenCalledWith(codeBlockMarkdown);
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to format a github codeblock (pasted as a table) with with existing draft post', () => {
|
|
||||||
const draft = emptyDraft;
|
|
||||||
const wrapper = shallow(
|
|
||||||
<AdvancedCreateComment
|
|
||||||
{...baseProps}
|
|
||||||
draft={draft}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockTop = () => {
|
|
||||||
return document.createElement('div');
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockImpl = () => {
|
|
||||||
return {
|
|
||||||
setSelectionRange: jest.fn(),
|
|
||||||
getBoundingClientRect: jest.fn(mockTop),
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.instance().textboxRef.current = {getInputBox: jest.fn(mockImpl), getBoundingClientRect: jest.fn(), focus: jest.fn()};
|
|
||||||
wrapper.setState({
|
|
||||||
draft: {
|
|
||||||
...draft,
|
|
||||||
message: 'test',
|
|
||||||
},
|
|
||||||
caretPosition: 'test'.length, // cursor is at the end
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
target: {
|
|
||||||
id: 'reply_textbox',
|
|
||||||
},
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
clipboardData: {
|
|
||||||
items: [1],
|
|
||||||
types: ['text/plain', 'text/html'],
|
|
||||||
getData: (type) => {
|
|
||||||
if (type === 'text/plain') {
|
|
||||||
return '// a javascript codeblock example\nif (1 > 0) {\n return \'condition is true\';\n}';
|
|
||||||
}
|
|
||||||
return '<table class="highlight tab-size js-file-line-container" data-tab-size="8"><tbody><tr><td id="LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-c"><span class="pl-c">//</span> a javascript codeblock example</span></td></tr><tr><td id="L2" class="blob-num js-line-number" data-line-number="2"> </td><td id="LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-k">if</span> (<span class="pl-c1">1</span> <span class="pl-k">></span> <span class="pl-c1">0</span>) {</td></tr><tr><td id="L3" class="blob-num js-line-number" data-line-number="3"> </td><td id="LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">\'</span>condition is true<span class="pl-pds">\'</span></span>);</td></tr><tr><td id="L4" class="blob-num js-line-number" data-line-number="4"> </td><td id="LC4" class="blob-code blob-code-inner js-file-line">}</td></tr></tbody></table>';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const codeBlockMarkdown = "test\n```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
|
||||||
expect(wrapper.state('draft').message).toBe(codeBlockMarkdown);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show preview and edit mode, and return focus on preview disable', () => {
|
test('should show preview and edit mode, and return focus on preview disable', () => {
|
||||||
|
@ -34,14 +34,20 @@ import {
|
|||||||
groupsMentionedInText,
|
groupsMentionedInText,
|
||||||
mentionsMinusSpecialMentionsInText,
|
mentionsMinusSpecialMentionsInText,
|
||||||
} from 'utils/post_utils';
|
} from 'utils/post_utils';
|
||||||
import {getTable, hasHtmlLink, formatMarkdownMessage, isGitHubCodeBlock, formatGithubCodePaste, isHttpProtocol, isHttpsProtocol} from 'utils/paste';
|
|
||||||
import EmojiMap from 'utils/emoji_map';
|
|
||||||
import {
|
import {
|
||||||
applyLinkMarkdown,
|
getHtmlTable,
|
||||||
ApplyLinkMarkdownOptions,
|
hasHtmlLink,
|
||||||
|
formatMarkdownMessage,
|
||||||
|
isGitHubCodeBlock,
|
||||||
|
formatGithubCodePaste,
|
||||||
|
isTextUrl,
|
||||||
|
formatMarkdownLinkMessage,
|
||||||
|
} from 'utils/paste';
|
||||||
|
import {
|
||||||
applyMarkdown,
|
applyMarkdown,
|
||||||
ApplyMarkdownOptions,
|
ApplyMarkdownOptions,
|
||||||
} from 'utils/markdown/apply_markdown';
|
} from 'utils/markdown/apply_markdown';
|
||||||
|
import {execCommandInsertText} from 'utils/exec_commands';
|
||||||
|
|
||||||
import NotifyConfirmModal from 'components/notify_confirm_modal';
|
import NotifyConfirmModal from 'components/notify_confirm_modal';
|
||||||
import {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
|
import {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
|
||||||
@ -180,13 +186,11 @@ type Props = {
|
|||||||
getChannelMemberCountsByGroup: (channelID: string, isTimezoneEnabled: boolean) => void;
|
getChannelMemberCountsByGroup: (channelID: string, isTimezoneEnabled: boolean) => void;
|
||||||
groupsWithAllowReference: Map<string, Group> | null;
|
groupsWithAllowReference: Map<string, Group> | null;
|
||||||
channelMemberCountsByGroup: ChannelMemberCountsByGroup;
|
channelMemberCountsByGroup: ChannelMemberCountsByGroup;
|
||||||
onHeightChange?: (height: number, maxHeight: number) => void;
|
|
||||||
focusOnMount?: boolean;
|
focusOnMount?: boolean;
|
||||||
isThreadView?: boolean;
|
isThreadView?: boolean;
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
savePreferences: (userId: string, preferences: PreferenceType[]) => ActionResult;
|
savePreferences: (userId: string, preferences: PreferenceType[]) => ActionResult;
|
||||||
useCustomGroupMentions: boolean;
|
useCustomGroupMentions: boolean;
|
||||||
emojiMap: EmojiMap;
|
|
||||||
isFormattingBarHidden: boolean;
|
isFormattingBarHidden: boolean;
|
||||||
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined) => Promise<{ data: any }>;
|
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined) => Promise<{ data: any }>;
|
||||||
}
|
}
|
||||||
@ -421,66 +425,41 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
pasteHandler = (e: ClipboardEvent) => {
|
pasteHandler = (event: ClipboardEvent) => {
|
||||||
// we need to cast the TextboxElement type onto the EventTarget here since the ClipboardEvent is not generic
|
const {clipboardData, target} = event;
|
||||||
if (!e.clipboardData || !e.clipboardData.items || (e.target as TextboxElement).id !== 'reply_textbox') {
|
|
||||||
|
if (!clipboardData || !clipboardData.items || !target || (target as TextboxElement)?.id !== 'reply_textbox') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {clipboardData} = e;
|
const {selectionStart, selectionEnd} = target as TextboxElement;
|
||||||
|
|
||||||
const target = e.target as TextboxElement;
|
|
||||||
|
|
||||||
const {selectionStart, selectionEnd, value} = target;
|
|
||||||
|
|
||||||
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
||||||
const clipboardText = clipboardData.getData('text/plain');
|
const hasTextUrl = isTextUrl(clipboardData);
|
||||||
const isClipboardTextURL = isHttpProtocol(clipboardText) || isHttpsProtocol(clipboardText);
|
const hasHTMLLinks = hasHtmlLink(clipboardData);
|
||||||
const shouldApplyLinkMarkdown = hasSelection && isClipboardTextURL;
|
const htmlTable = getHtmlTable(clipboardData);
|
||||||
|
const shouldApplyLinkMarkdown = hasSelection && hasTextUrl;
|
||||||
|
const shouldApplyGithubCodeBlock = htmlTable && isGitHubCodeBlock(htmlTable.className);
|
||||||
|
|
||||||
const hasLinks = hasHtmlLink(clipboardData);
|
if (!htmlTable && !hasHTMLLinks && !shouldApplyLinkMarkdown) {
|
||||||
let table = getTable(clipboardData);
|
|
||||||
if (!table && !hasLinks && !shouldApplyLinkMarkdown) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
const message = this.state.draft?.message ?? '';
|
||||||
|
|
||||||
|
// execCommand's insertText' triggers a 'change' event, hence we need not set respective state explicitly.
|
||||||
if (shouldApplyLinkMarkdown) {
|
if (shouldApplyLinkMarkdown) {
|
||||||
this.applyLinkMarkdownWhenPaste({
|
const formattedLink = formatMarkdownLinkMessage({selectionStart, selectionEnd, message, clipboardData});
|
||||||
selectionStart,
|
execCommandInsertText(formattedLink);
|
||||||
selectionEnd,
|
} else if (shouldApplyGithubCodeBlock) {
|
||||||
message: value,
|
const {formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
||||||
url: clipboardText,
|
execCommandInsertText(formattedCodeBlock);
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
table = table as HTMLTableElement;
|
|
||||||
|
|
||||||
const draft = this.state.draft!;
|
|
||||||
let message = draft.message;
|
|
||||||
|
|
||||||
const caretPosition = this.state.caretPosition || 0;
|
|
||||||
if (table && isGitHubCodeBlock(table.className)) {
|
|
||||||
const selectionStart = (e.target as any).selectionStart;
|
|
||||||
const selectionEnd = (e.target as any).selectionEnd;
|
|
||||||
const {formattedMessage, formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
|
||||||
const newCaretPosition = caretPosition + formattedCodeBlock.length;
|
|
||||||
message = formattedMessage;
|
|
||||||
this.setCaretPosition(newCaretPosition);
|
|
||||||
} else {
|
} else {
|
||||||
const originalSize = draft.message.length;
|
const {formattedMarkdown} = formatMarkdownMessage(clipboardData, message, this.state.caretPosition);
|
||||||
message = formatMarkdownMessage(clipboardData, draft.message.trim(), this.state.caretPosition);
|
execCommandInsertText(formattedMarkdown);
|
||||||
const newCaretPosition = message.length - (originalSize - caretPosition);
|
|
||||||
this.setCaretPosition(newCaretPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDraft = {...draft, message};
|
|
||||||
|
|
||||||
this.handleDraftChange(updatedDraft);
|
|
||||||
this.setState({draft: updatedDraft});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleNotifyAllConfirmation = () => {
|
handleNotifyAllConfirmation = () => {
|
||||||
@ -1044,25 +1023,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
applyLinkMarkdownWhenPaste = (params: ApplyLinkMarkdownOptions) => {
|
|
||||||
const res = applyLinkMarkdown(params);
|
|
||||||
|
|
||||||
const draft = this.state.draft!;
|
|
||||||
const modifiedDraft = {
|
|
||||||
...draft,
|
|
||||||
message: res.message,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleDraftChange(modifiedDraft);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
draft: modifiedDraft,
|
|
||||||
}, () => {
|
|
||||||
const textbox = this.textboxRef.current?.getInputBox();
|
|
||||||
Utils.setSelectionRange(textbox, res.selectionEnd + 1, res.selectionEnd + 1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFileUploadChange = () => {
|
handleFileUploadChange = () => {
|
||||||
this.isDraftEdited = true;
|
this.isDraftEdited = true;
|
||||||
this.focusTextbox();
|
this.focusTextbox();
|
||||||
|
@ -46,7 +46,6 @@ import {setShowPreviewOnCreateComment} from 'actions/views/textbox';
|
|||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
||||||
|
|
||||||
import {getEmojiMap} from 'selectors/emojis';
|
|
||||||
import {canUploadFiles} from 'utils/file_utils';
|
import {canUploadFiles} from 'utils/file_utils';
|
||||||
|
|
||||||
import AdvancedCreateComment from './advanced_create_comment';
|
import AdvancedCreateComment from './advanced_create_comment';
|
||||||
@ -116,7 +115,6 @@ function makeMapStateToProps() {
|
|||||||
useLDAPGroupMentions,
|
useLDAPGroupMentions,
|
||||||
channelMemberCountsByGroup,
|
channelMemberCountsByGroup,
|
||||||
useCustomGroupMentions,
|
useCustomGroupMentions,
|
||||||
emojiMap: getEmojiMap(state),
|
|
||||||
canUploadFiles: canUploadFiles(config),
|
canUploadFiles: canUploadFiles(config),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,17 +4,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import AdvancedCreatePost from 'components/advanced_create_post/advanced_create_post';
|
|
||||||
|
|
||||||
import {Posts} from 'mattermost-redux/constants';
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
||||||
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
||||||
import * as GlobalActions from 'actions/global_actions';
|
|
||||||
import EmojiMap from 'utils/emoji_map';
|
|
||||||
|
|
||||||
|
import * as GlobalActions from 'actions/global_actions';
|
||||||
|
|
||||||
|
import EmojiMap from 'utils/emoji_map';
|
||||||
import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants';
|
import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
import AdvanceTextEditor from '../advanced_text_editor/advanced_text_editor';
|
import {execCommandInsertText} from 'utils/exec_commands';
|
||||||
|
|
||||||
|
import AdvancedCreatePost from 'components/advanced_create_post/advanced_create_post';
|
||||||
|
import AdvanceTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||||
|
|
||||||
jest.mock('actions/global_actions', () => ({
|
jest.mock('actions/global_actions', () => ({
|
||||||
emitLocalUserTypingEvent: jest.fn(),
|
emitLocalUserTypingEvent: jest.fn(),
|
||||||
@ -29,6 +32,10 @@ jest.mock('actions/post_actions', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('utils/exec_commands', () => ({
|
||||||
|
execCommandInsertText: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const currentTeamIdProp = 'r7rws4y7ppgszym3pdd5kaibfa';
|
const currentTeamIdProp = 'r7rws4y7ppgszym3pdd5kaibfa';
|
||||||
const currentUserIdProp = 'zaktnt8bpbgu8mb6ez9k64r7sa';
|
const currentUserIdProp = 'zaktnt8bpbgu8mb6ez9k64r7sa';
|
||||||
const showTutorialTipProp = false;
|
const showTutorialTipProp = false;
|
||||||
@ -82,7 +89,6 @@ const actionsProp = {
|
|||||||
searchAssociatedGroupsForReference: jest.fn(),
|
searchAssociatedGroupsForReference: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
function advancedCreatePost({
|
function advancedCreatePost({
|
||||||
currentChannel = currentChannelProp,
|
currentChannel = currentChannelProp,
|
||||||
currentTeamId = currentTeamIdProp,
|
currentTeamId = currentTeamIdProp,
|
||||||
@ -145,7 +151,6 @@ function advancedCreatePost({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/* eslint-enable react/prop-types */
|
|
||||||
|
|
||||||
describe('components/advanced_create_post', () => {
|
describe('components/advanced_create_post', () => {
|
||||||
jest.useFakeTimers('legacy');
|
jest.useFakeTimers('legacy');
|
||||||
@ -1288,7 +1293,7 @@ describe('components/advanced_create_post', () => {
|
|||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('message')).toBe(markdownTable);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a pasted markdown table without headers', () => {
|
it('should be able to format a pasted markdown table without headers', () => {
|
||||||
@ -1318,7 +1323,7 @@ describe('components/advanced_create_post', () => {
|
|||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('message')).toBe(markdownTable);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a pasted hyperlink', () => {
|
it('should be able to format a pasted hyperlink', () => {
|
||||||
@ -1348,51 +1353,7 @@ describe('components/advanced_create_post', () => {
|
|||||||
const markdownLink = '[link text](https://test.domain)';
|
const markdownLink = '[link text](https://test.domain)';
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('message')).toBe(markdownLink);
|
expect(execCommandInsertText).toHaveBeenCalledWith(markdownLink);
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve the original message after pasting a markdown table', () => {
|
|
||||||
const wrapper = shallow(advancedCreatePost());
|
|
||||||
|
|
||||||
const message = 'original message';
|
|
||||||
wrapper.setState({
|
|
||||||
message,
|
|
||||||
caretPosition: message.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
target: {
|
|
||||||
id: 'post_textbox',
|
|
||||||
},
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
clipboardData: {
|
|
||||||
items: [1],
|
|
||||||
types: ['text/html'],
|
|
||||||
getData: () => {
|
|
||||||
return '<table><tr><td>test</td><td>test</td></tr><tr><td>test</td><td>test</td></tr></table>';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n\n';
|
|
||||||
const expectedMessage = `${message}\n\n${markdownTable}`;
|
|
||||||
|
|
||||||
const mockTop = () => {
|
|
||||||
return document.createElement('div');
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockImpl = () => {
|
|
||||||
return {
|
|
||||||
setSelectionRange: jest.fn(),
|
|
||||||
getBoundingClientRect: jest.fn(mockTop),
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.instance().textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
|
||||||
expect(wrapper.state('message')).toBe(expectedMessage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to format a github codeblock (pasted as a table)', () => {
|
it('should be able to format a github codeblock (pasted as a table)', () => {
|
||||||
@ -1425,95 +1386,7 @@ describe('components/advanced_create_post', () => {
|
|||||||
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
wrapper.instance().pasteHandler(event);
|
||||||
expect(wrapper.state('message')).toBe(codeBlockMarkdown);
|
expect(execCommandInsertText).toHaveBeenCalledWith(codeBlockMarkdown);
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to format a github codeblock (pasted as a table) with existing draft post', () => {
|
|
||||||
const wrapper = shallow(advancedCreatePost());
|
|
||||||
const mockImpl = () => {
|
|
||||||
return {
|
|
||||||
setSelectionRange: jest.fn(),
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
wrapper.instance().textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
|
||||||
wrapper.setState({
|
|
||||||
message: 'test',
|
|
||||||
caretPosition: 'test'.length, // cursor is at the end
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
target: {
|
|
||||||
id: 'post_textbox',
|
|
||||||
},
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
clipboardData: {
|
|
||||||
items: [1],
|
|
||||||
types: ['text/plain', 'text/html'],
|
|
||||||
getData: (type) => {
|
|
||||||
if (type === 'text/plain') {
|
|
||||||
return '// a javascript codeblock example\nif (1 > 0) {\n return \'condition is true\';\n}';
|
|
||||||
}
|
|
||||||
return '<table class="highlight tab-size js-file-line-container" data-tab-size="8"><tbody><tr><td id="LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-c"><span class="pl-c">//</span> a javascript codeblock example</span></td></tr><tr><td id="L2" class="blob-num js-line-number" data-line-number="2"> </td><td id="LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-k">if</span> (<span class="pl-c1">1</span> <span class="pl-k">></span> <span class="pl-c1">0</span>) {</td></tr><tr><td id="L3" class="blob-num js-line-number" data-line-number="3"> </td><td id="LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">\'</span>condition is true<span class="pl-pds">\'</span></span>);</td></tr><tr><td id="L4" class="blob-num js-line-number" data-line-number="4"> </td><td id="LC4" class="blob-code blob-code-inner js-file-line">}</td></tr></tbody></table>';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const codeBlockMarkdown = "test\n```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
|
||||||
expect(wrapper.state('message')).toBe(codeBlockMarkdown);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call handlePostPasteDraft to update the draft after pasting', () => {
|
|
||||||
const wrapper = shallow(advancedCreatePost());
|
|
||||||
const mockImpl = () => {
|
|
||||||
return {
|
|
||||||
setSelectionRange: jest.fn(),
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
wrapper.instance().textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
|
||||||
wrapper.instance().handlePostPasteDraft = jest.fn();
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
target: {
|
|
||||||
id: 'post_textbox',
|
|
||||||
},
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
clipboardData: {
|
|
||||||
items: [1],
|
|
||||||
types: ['text/html'],
|
|
||||||
getData: () => {
|
|
||||||
return '<a href="https://test.domain">link text</a>';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.instance().pasteHandler(event);
|
|
||||||
expect(wrapper.instance().handlePostPasteDraft).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update draft when handlePostPasteDraft is called', () => {
|
|
||||||
const setDraft = jest.fn();
|
|
||||||
|
|
||||||
const wrapper = shallow(
|
|
||||||
advancedCreatePost({
|
|
||||||
actions: {
|
|
||||||
...actionsProp,
|
|
||||||
setDraft,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const testMessage = 'test';
|
|
||||||
const expectedDraft = {
|
|
||||||
...draftProp,
|
|
||||||
message: testMessage,
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.instance().handlePostPasteDraft(testMessage);
|
|
||||||
expect(setDraft).toHaveBeenCalledWith(StoragePrefixes.DRAFT + currentChannelProp.id, expectedDraft, currentChannelProp.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,13 +4,21 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {isNil} from 'lodash';
|
import {isNil} from 'lodash';
|
||||||
|
|
||||||
import {Posts} from 'mattermost-redux/constants';
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
||||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
|
import {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
||||||
|
import {Post, PostMetadata, PostPriority, PostPriorityMetadata} from '@mattermost/types/posts';
|
||||||
|
import {PreferenceType} from '@mattermost/types/preferences';
|
||||||
|
import {ServerError} from '@mattermost/types/errors';
|
||||||
|
import {CommandArgs} from '@mattermost/types/integrations';
|
||||||
|
import {Group, GroupSource} from '@mattermost/types/groups';
|
||||||
|
import {FileInfo} from '@mattermost/types/files';
|
||||||
|
import {Emoji} from '@mattermost/types/emojis';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions';
|
import * as GlobalActions from 'actions/global_actions';
|
||||||
import Constants, {
|
import Constants, {
|
||||||
StoragePrefixes,
|
StoragePrefixes,
|
||||||
@ -32,11 +40,20 @@ import {
|
|||||||
mentionsMinusSpecialMentionsInText,
|
mentionsMinusSpecialMentionsInText,
|
||||||
hasRequestedPersistentNotifications,
|
hasRequestedPersistentNotifications,
|
||||||
} from 'utils/post_utils';
|
} from 'utils/post_utils';
|
||||||
import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock, isHttpProtocol, isHttpsProtocol} from 'utils/paste';
|
import {
|
||||||
|
getHtmlTable,
|
||||||
|
hasHtmlLink,
|
||||||
|
formatMarkdownMessage,
|
||||||
|
formatGithubCodePaste,
|
||||||
|
isGitHubCodeBlock,
|
||||||
|
formatMarkdownLinkMessage,
|
||||||
|
isTextUrl,
|
||||||
|
} from 'utils/paste';
|
||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
import EmojiMap from 'utils/emoji_map';
|
import EmojiMap from 'utils/emoji_map';
|
||||||
import {applyLinkMarkdown, ApplyLinkMarkdownOptions, applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||||
|
import {execCommandInsertText} from 'utils/exec_commands';
|
||||||
|
|
||||||
import NotifyConfirmModal from 'components/notify_confirm_modal';
|
import NotifyConfirmModal from 'components/notify_confirm_modal';
|
||||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||||
@ -48,24 +65,11 @@ import PostPriorityPickerOverlay from 'components/post_priority/post_priority_pi
|
|||||||
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
|
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
|
||||||
|
|
||||||
import {PostDraft} from 'types/store/draft';
|
import {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
import {ModalData} from 'types/actions';
|
import {ModalData} from 'types/actions';
|
||||||
|
|
||||||
import {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
|
||||||
import {Post, PostMetadata, PostPriority, PostPriorityMetadata} from '@mattermost/types/posts';
|
|
||||||
import {PreferenceType} from '@mattermost/types/preferences';
|
|
||||||
import {ServerError} from '@mattermost/types/errors';
|
|
||||||
import {CommandArgs} from '@mattermost/types/integrations';
|
|
||||||
import {Group, GroupSource} from '@mattermost/types/groups';
|
|
||||||
import {FileInfo} from '@mattermost/types/files';
|
|
||||||
import {Emoji} from '@mattermost/types/emojis';
|
|
||||||
|
|
||||||
import AdvancedTextEditor from '../advanced_text_editor/advanced_text_editor';
|
import AdvancedTextEditor from '../advanced_text_editor/advanced_text_editor';
|
||||||
|
|
||||||
import FileLimitStickyBanner from '../file_limit_sticky_banner';
|
import FileLimitStickyBanner from '../file_limit_sticky_banner';
|
||||||
|
|
||||||
import {FilePreviewInfo} from '../file_preview/file_preview';
|
import {FilePreviewInfo} from '../file_preview/file_preview';
|
||||||
|
|
||||||
import PriorityLabels from './priority_labels';
|
import PriorityLabels from './priority_labels';
|
||||||
|
|
||||||
const KeyCodes = Constants.KeyCodes;
|
const KeyCodes = Constants.KeyCodes;
|
||||||
@ -74,15 +78,6 @@ function isDraftEmpty(draft: PostDraft): boolean {
|
|||||||
return !draft || (!draft.message && draft.fileInfos.length === 0);
|
return !draft || (!draft.message && draft.fileInfos.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for IE-11, see MM-13423
|
|
||||||
function trimRight(str: string) {
|
|
||||||
if (String.prototype.trimRight as any) {
|
|
||||||
return str.trimRight();
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.replace(/\s*$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextboxElement = HTMLInputElement | HTMLTextAreaElement;
|
type TextboxElement = HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -713,7 +708,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimRight(this.state.message) === '/header') {
|
if (this.state.message.trimEnd() === '/header') {
|
||||||
const editChannelHeaderModalData = {
|
const editChannelHeaderModalData = {
|
||||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||||
dialogType: EditChannelHeaderModal,
|
dialogType: EditChannelHeaderModal,
|
||||||
@ -727,7 +722,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDirectOrGroup && trimRight(this.state.message) === '/purpose') {
|
if (!isDirectOrGroup && this.state.message.trimEnd() === '/purpose') {
|
||||||
const editChannelPurposeModalData = {
|
const editChannelPurposeModalData = {
|
||||||
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
||||||
dialogType: EditChannelPurposeModal,
|
dialogType: EditChannelPurposeModal,
|
||||||
@ -919,74 +914,41 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
this.draftsForChannel[channelId] = draft;
|
this.draftsForChannel[channelId] = draft;
|
||||||
};
|
};
|
||||||
|
|
||||||
pasteHandler = (e: ClipboardEvent) => {
|
pasteHandler = (event: ClipboardEvent) => {
|
||||||
/**
|
const {clipboardData, target} = event;
|
||||||
* casting HTMLInputElement on the EventTarget was necessary here
|
|
||||||
* since the ClipboardEvent type is not generic
|
if (!clipboardData || !clipboardData.items || !target || ((target as TextboxElement)?.id !== 'post_textbox')) {
|
||||||
*/
|
|
||||||
if (!e.clipboardData || !e.clipboardData.items || ((e.target as TextboxElement)?.id !== 'post_textbox')) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {clipboardData} = e;
|
const {selectionStart, selectionEnd} = target as TextboxElement;
|
||||||
|
|
||||||
const target = e.target as TextboxElement;
|
|
||||||
|
|
||||||
const {selectionStart, selectionEnd, value} = target;
|
|
||||||
|
|
||||||
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
||||||
const clipboardText = clipboardData.getData('text/plain');
|
const hasTextUrl = isTextUrl(clipboardData);
|
||||||
const isClipboardTextURL = isHttpProtocol(clipboardText) || isHttpsProtocol(clipboardText);
|
const hasHTMLLinks = hasHtmlLink(clipboardData);
|
||||||
const shouldApplyLinkMarkdown = hasSelection && isClipboardTextURL;
|
const htmlTable = getHtmlTable(clipboardData);
|
||||||
|
const shouldApplyLinkMarkdown = hasSelection && hasTextUrl;
|
||||||
|
const shouldApplyGithubCodeBlock = htmlTable && isGitHubCodeBlock(htmlTable.className);
|
||||||
|
|
||||||
const hasLinks = hasHtmlLink(clipboardData);
|
if (!htmlTable && !hasHTMLLinks && !shouldApplyLinkMarkdown) {
|
||||||
let table = getTable(clipboardData);
|
|
||||||
|
|
||||||
if (!table && !hasLinks && !shouldApplyLinkMarkdown) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (shouldApplyLinkMarkdown) {
|
|
||||||
this.applyLinkMarkdownWhenPaste({
|
|
||||||
selectionStart,
|
|
||||||
selectionEnd,
|
|
||||||
message: value,
|
|
||||||
url: clipboardText,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
table = table as HTMLTableElement;
|
|
||||||
|
|
||||||
const message = this.state.message;
|
const message = this.state.message;
|
||||||
if (table && isGitHubCodeBlock(table.className)) {
|
|
||||||
const selectionStart = (e.target as any).selectionStart;
|
// execCommand's insertText' triggers a 'change' event, hence we need not set respective state explicitly.
|
||||||
const selectionEnd = (e.target as any).selectionEnd;
|
if (shouldApplyLinkMarkdown) {
|
||||||
const {formattedMessage, formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
const formattedLink = formatMarkdownLinkMessage({selectionStart, selectionEnd, message, clipboardData});
|
||||||
const newCaretPosition = this.state.caretPosition + formattedCodeBlock.length;
|
execCommandInsertText(formattedLink);
|
||||||
this.setMessageAndCaretPostion(formattedMessage, newCaretPosition);
|
} else if (shouldApplyGithubCodeBlock) {
|
||||||
return;
|
const {formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
||||||
|
execCommandInsertText(formattedCodeBlock);
|
||||||
|
} else {
|
||||||
|
const {formattedMarkdown} = formatMarkdownMessage(clipboardData, message, this.state.caretPosition);
|
||||||
|
execCommandInsertText(formattedMarkdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalSize = message.length;
|
|
||||||
const formattedMessage = formatMarkdownMessage(clipboardData, message.trim(), this.state.caretPosition);
|
|
||||||
const newCaretPosition = formattedMessage.length - (originalSize - this.state.caretPosition);
|
|
||||||
this.setMessageAndCaretPostion(formattedMessage, newCaretPosition);
|
|
||||||
this.handlePostPasteDraft(formattedMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePostPasteDraft = (message: string) => {
|
|
||||||
const draft = {
|
|
||||||
...this.props.draft,
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const channelId = this.props.currentChannel.id;
|
|
||||||
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, draft, channelId);
|
|
||||||
this.draftsForChannel[channelId] = draft;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFileUploadChange = () => {
|
handleFileUploadChange = () => {
|
||||||
@ -1410,24 +1372,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
applyLinkMarkdownWhenPaste = (params: ApplyLinkMarkdownOptions) => {
|
|
||||||
const res = applyLinkMarkdown(params);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
message: res.message,
|
|
||||||
}, () => {
|
|
||||||
const textbox = this.textboxRef.current?.getInputBox();
|
|
||||||
Utils.setSelectionRange(textbox, res.selectionEnd + 1, res.selectionEnd + 1);
|
|
||||||
|
|
||||||
const draft = {
|
|
||||||
...this.props.draft,
|
|
||||||
message: this.state.message,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleDraftChange(draft);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
reactToLastMessage = (e: KeyboardEvent) => {
|
reactToLastMessage = (e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import * as Keyboard from 'utils/keyboard';
|
|||||||
import {
|
import {
|
||||||
formatGithubCodePaste,
|
formatGithubCodePaste,
|
||||||
formatMarkdownMessage,
|
formatMarkdownMessage,
|
||||||
getTable,
|
getHtmlTable,
|
||||||
hasHtmlLink,
|
hasHtmlLink,
|
||||||
isGitHubCodeBlock,
|
isGitHubCodeBlock,
|
||||||
} from 'utils/paste';
|
} from 'utils/paste';
|
||||||
@ -163,7 +163,7 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasLinks = hasHtmlLink(clipboardData);
|
const hasLinks = hasHtmlLink(clipboardData);
|
||||||
const table = getTable(clipboardData);
|
const table = getHtmlTable(clipboardData);
|
||||||
if (!table && !hasLinks) {
|
if (!table && !hasLinks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -178,7 +178,7 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
|||||||
message = formattedMessage;
|
message = formattedMessage;
|
||||||
newCaretPosition = selectionRange.start + formattedCodeBlock.length;
|
newCaretPosition = selectionRange.start + formattedCodeBlock.length;
|
||||||
} else {
|
} else {
|
||||||
message = formatMarkdownMessage(clipboardData, editText.trim(), newCaretPosition);
|
message = formatMarkdownMessage(clipboardData, editText.trim(), newCaretPosition).formattedMessage;
|
||||||
newCaretPosition = message.length - (editText.length - newCaretPosition);
|
newCaretPosition = message.length - (editText.length - newCaretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
isIosChrome,
|
isIosChrome,
|
||||||
isMobileApp,
|
isMobileApp,
|
||||||
} from 'utils/user_agent';
|
} from 'utils/user_agent';
|
||||||
import {getTable} from 'utils/paste';
|
import {getHtmlTable} from 'utils/paste';
|
||||||
import {
|
import {
|
||||||
clearFileInput,
|
clearFileInput,
|
||||||
generateId,
|
generateId,
|
||||||
@ -451,7 +451,7 @@ export class FileUpload extends PureComponent<Props, State> {
|
|||||||
pasteUpload = (e: ClipboardEvent) => {
|
pasteUpload = (e: ClipboardEvent) => {
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
if (!e.clipboardData || !e.clipboardData.items || getTable(e.clipboardData)) {
|
if (!e.clipboardData || !e.clipboardData.items || getHtmlTable(e.clipboardData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ import BasicSeparator from 'components/widgets/separator/basic-separator';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
focusOnMount: boolean;
|
focusOnMount: boolean;
|
||||||
onHeightChange: (height: number, maxHeight: number) => void;
|
|
||||||
teammate?: UserProfile;
|
teammate?: UserProfile;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
latestPostId: Post['id'];
|
latestPostId: Post['id'];
|
||||||
@ -29,7 +28,6 @@ type Props = {
|
|||||||
|
|
||||||
const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
||||||
focusOnMount,
|
focusOnMount,
|
||||||
onHeightChange,
|
|
||||||
teammate,
|
teammate,
|
||||||
threadId,
|
threadId,
|
||||||
latestPostId,
|
latestPostId,
|
||||||
@ -97,7 +95,6 @@ const CreateComment = forwardRef<HTMLDivElement, Props>(({
|
|||||||
focusOnMount={focusOnMount}
|
focusOnMount={focusOnMount}
|
||||||
channelId={channel.id}
|
channelId={channel.id}
|
||||||
latestPostId={latestPostId}
|
latestPostId={latestPostId}
|
||||||
onHeightChange={onHeightChange}
|
|
||||||
rootDeleted={rootDeleted}
|
rootDeleted={rootDeleted}
|
||||||
rootId={threadId}
|
rootId={threadId}
|
||||||
isThreadView={isThreadView}
|
isThreadView={isThreadView}
|
||||||
|
@ -63,8 +63,6 @@ const innerStyles = {
|
|||||||
paddingTop: '28px',
|
paddingTop: '28px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CREATE_COMMENT_BUTTON_HEIGHT = 81;
|
|
||||||
|
|
||||||
const THREADING_TIME: typeof BASE_THREADING_TIME = {
|
const THREADING_TIME: typeof BASE_THREADING_TIME = {
|
||||||
...BASE_THREADING_TIME,
|
...BASE_THREADING_TIME,
|
||||||
units: [
|
units: [
|
||||||
@ -334,18 +332,6 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCreateCommentHeightChange = (height: number, maxHeight: number) => {
|
|
||||||
let createCommentHeight = height > maxHeight ? maxHeight : height;
|
|
||||||
createCommentHeight += CREATE_COMMENT_BUTTON_HEIGHT;
|
|
||||||
|
|
||||||
if (createCommentHeight !== this.state.createCommentHeight) {
|
|
||||||
this.setState({createCommentHeight});
|
|
||||||
if (this.state.userScrolledToBottom) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderRow = ({data, itemId, style}: {data: any; itemId: any; style: any}) => {
|
renderRow = ({data, itemId, style}: {data: any; itemId: any; style: any}) => {
|
||||||
const index = data.indexOf(itemId);
|
const index = data.indexOf(itemId);
|
||||||
let className = '';
|
let className = '';
|
||||||
@ -379,7 +365,6 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
|||||||
focusOnMount={!this.props.isThreadView && (this.state.userScrolledToBottom || (!this.state.userScrolled && this.getInitialPostIndex() === 0))}
|
focusOnMount={!this.props.isThreadView && (this.state.userScrolledToBottom || (!this.state.userScrolled && this.getInitialPostIndex() === 0))}
|
||||||
isThreadView={this.props.isThreadView}
|
isThreadView={this.props.isThreadView}
|
||||||
latestPostId={this.props.lastPost.id}
|
latestPostId={this.props.lastPost.id}
|
||||||
onHeightChange={this.handleCreateCommentHeightChange}
|
|
||||||
ref={this.postCreateContainerRef}
|
ref={this.postCreateContainerRef}
|
||||||
teammate={this.props.directTeammate}
|
teammate={this.props.directTeammate}
|
||||||
threadId={this.props.selected.id}
|
threadId={this.props.selected.id}
|
||||||
|
@ -29,7 +29,7 @@ Object.defineProperty(window, 'location', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const supportedCommands = ['copy'];
|
const supportedCommands = ['copy', 'insertText'];
|
||||||
|
|
||||||
Object.defineProperty(document, 'queryCommandSupported', {
|
Object.defineProperty(document, 'queryCommandSupported', {
|
||||||
value: (cmd) => supportedCommands.includes(cmd),
|
value: (cmd) => supportedCommands.includes(cmd),
|
||||||
|
10
webapp/channels/src/utils/exec_commands.ts
Normal file
10
webapp/channels/src/utils/exec_commands.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a wrapper around document.execCommand('insertText', false, text) to insert test into the focused element.
|
||||||
|
* @param text The text to insert.
|
||||||
|
*/
|
||||||
|
export function execCommandInsertText(text: string) {
|
||||||
|
document.execCommand('insertText', false, text);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {parseTable, getTable, formatMarkdownMessage, formatGithubCodePaste} from './paste';
|
import {parseHtmlTable, getHtmlTable, formatMarkdownMessage, formatGithubCodePaste, formatMarkdownLinkMessage, isTextUrl} from './paste';
|
||||||
|
|
||||||
const validClipboardData: any = {
|
const validClipboardData: any = {
|
||||||
items: [1],
|
items: [1],
|
||||||
@ -11,16 +11,16 @@ const validClipboardData: any = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const validTable: any = parseTable(validClipboardData.getData());
|
const validTable: any = parseHtmlTable(validClipboardData.getData());
|
||||||
|
|
||||||
describe('Paste.getTable', () => {
|
describe('getHtmlTable', () => {
|
||||||
test('returns false without html in the clipboard', () => {
|
test('returns false without html in the clipboard', () => {
|
||||||
const badClipboardData: any = {
|
const badClipboardData: any = {
|
||||||
items: [1],
|
items: [1],
|
||||||
types: ['text/plain'],
|
types: ['text/plain'],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getTable(badClipboardData)).toBe(null);
|
expect(getHtmlTable(badClipboardData)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns false without table in the clipboard', () => {
|
test('returns false without table in the clipboard', () => {
|
||||||
@ -30,19 +30,19 @@ describe('Paste.getTable', () => {
|
|||||||
getData: () => '<p>There is no table here</p>',
|
getData: () => '<p>There is no table here</p>',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getTable(badClipboardData)).toBe(null);
|
expect(getHtmlTable(badClipboardData)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns table from valid clipboard data', () => {
|
test('returns table from valid clipboard data', () => {
|
||||||
expect(getTable(validClipboardData)).toEqual(validTable);
|
expect(getHtmlTable(validClipboardData)).toEqual(validTable);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Paste.formatMarkdownMessage', () => {
|
describe('formatMarkdownMessage', () => {
|
||||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
||||||
|
|
||||||
test('returns a markdown table when valid html table provided', () => {
|
test('returns a markdown table when valid html table provided', () => {
|
||||||
expect(formatMarkdownMessage(validClipboardData)).toBe(`${markdownTable}\n`);
|
expect(formatMarkdownMessage(validClipboardData).formattedMessage).toBe(`${markdownTable}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns a markdown table when valid html table with headers provided', () => {
|
test('returns a markdown table when valid html table with headers provided', () => {
|
||||||
@ -54,7 +54,7 @@ describe('Paste.formatMarkdownMessage', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(formatMarkdownMessage(tableHeadersClipboardData)).toBe(markdownTable);
|
expect(formatMarkdownMessage(tableHeadersClipboardData).formattedMessage).toBe(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('removes style contents and additional whitespace around tables', () => {
|
test('removes style contents and additional whitespace around tables', () => {
|
||||||
@ -66,13 +66,13 @@ describe('Paste.formatMarkdownMessage', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(formatMarkdownMessage(styleClipboardData)).toBe(markdownTable);
|
expect(formatMarkdownMessage(styleClipboardData).formattedMessage).toBe(markdownTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns a markdown table under a message when one is provided', () => {
|
test('returns a markdown table under a message when one is provided', () => {
|
||||||
const testMessage = 'test message';
|
const testMessage = 'test message';
|
||||||
|
|
||||||
expect(formatMarkdownMessage(validClipboardData, testMessage)).toBe(`${testMessage}\n\n${markdownTable}\n`);
|
expect(formatMarkdownMessage(validClipboardData, testMessage).formattedMessage).toBe(`${testMessage}\n\n${markdownTable}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns a markdown formatted link when valid hyperlink provided', () => {
|
test('returns a markdown formatted link when valid hyperlink provided', () => {
|
||||||
@ -85,11 +85,11 @@ describe('Paste.formatMarkdownMessage', () => {
|
|||||||
};
|
};
|
||||||
const markdownLink = '[link text](https://test.domain)';
|
const markdownLink = '[link text](https://test.domain)';
|
||||||
|
|
||||||
expect(formatMarkdownMessage(linkClipboardData)).toBe(markdownLink);
|
expect(formatMarkdownMessage(linkClipboardData).formattedMessage).toBe(markdownLink);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Paste.formatGithubCodePaste', () => {
|
describe('formatGithubCodePaste', () => {
|
||||||
const clipboardData: any = {
|
const clipboardData: any = {
|
||||||
items: [],
|
items: [],
|
||||||
types: ['text/plain', 'text/html'],
|
types: ['text/plain', 'text/html'],
|
||||||
@ -147,3 +147,50 @@ describe('Paste.formatGithubCodePaste', () => {
|
|||||||
expect(codeBlock).toBe(formattedCodeBlock);
|
expect(codeBlock).toBe(formattedCodeBlock);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatMarkdownLinkMessage', () => {
|
||||||
|
const clipboardData: any = {
|
||||||
|
items: [],
|
||||||
|
types: ['text/plain'],
|
||||||
|
getData: () => {
|
||||||
|
return 'https://example.com/';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Should return empty selection when no selection is made', () => {
|
||||||
|
const message = '';
|
||||||
|
|
||||||
|
const formatttedMarkdownLinkMessage = formatMarkdownLinkMessage({selectionStart: 0, selectionEnd: 0, message, clipboardData});
|
||||||
|
expect(formatttedMarkdownLinkMessage).toEqual('[](https://example.com/)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should return correct selection when selection is made', () => {
|
||||||
|
const message = 'test';
|
||||||
|
|
||||||
|
const formatttedMarkdownLinkMessage = formatMarkdownLinkMessage({selectionStart: 0, selectionEnd: 4, message, clipboardData});
|
||||||
|
expect(formatttedMarkdownLinkMessage).toEqual('[test](https://example.com/)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isTextUrl', () => {
|
||||||
|
test('Should return true when url is valid', () => {
|
||||||
|
const clipboardData: any = {
|
||||||
|
...validClipboardData,
|
||||||
|
getData: () => {
|
||||||
|
return 'https://example.com/';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isTextUrl(clipboardData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should return false when url is invalid', () => {
|
||||||
|
const clipboardData: any = {
|
||||||
|
...validClipboardData,
|
||||||
|
getData: () => {
|
||||||
|
return 'not a url';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(isTextUrl(clipboardData)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import TurndownService from 'turndown';
|
import turndownService from 'utils/turndown';
|
||||||
import {tables} from '@guyplusplus/turndown-plugin-gfm';
|
|
||||||
|
|
||||||
import {splitMessageBasedOnCaretPosition, splitMessageBasedOnTextSelection} from 'utils/post_utils';
|
import {splitMessageBasedOnCaretPosition, splitMessageBasedOnTextSelection} from 'utils/post_utils';
|
||||||
|
|
||||||
type FormatCodeOptions = {
|
type FormatMarkdownParams = {
|
||||||
message: string;
|
message: string;
|
||||||
clipboardData: DataTransfer;
|
clipboardData: DataTransfer;
|
||||||
selectionStart: number | null;
|
selectionStart: number | null;
|
||||||
selectionEnd: number | null;
|
selectionEnd: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseTable(html: string): HTMLTableElement | null {
|
export function parseHtmlTable(html: string): HTMLTableElement | null {
|
||||||
return new DOMParser().parseFromString(html, 'text/html').querySelector('table');
|
return new DOMParser().parseFromString(html, 'text/html').querySelector('table');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTable(clipboardData: DataTransfer): HTMLTableElement | null {
|
export function getHtmlTable(clipboardData: DataTransfer): HTMLTableElement | null {
|
||||||
if (Array.from(clipboardData.types).indexOf('text/html') === -1) {
|
if (Array.from(clipboardData.types).indexOf('text/html') === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -28,7 +26,7 @@ export function getTable(clipboardData: DataTransfer): HTMLTableElement | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = parseTable(html);
|
const table = parseHtmlTable(html);
|
||||||
if (!table) {
|
if (!table) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -40,66 +38,87 @@ export function hasHtmlLink(clipboardData: DataTransfer): boolean {
|
|||||||
return Array.from(clipboardData.types).includes('text/html') && (/<a/i).test(clipboardData.getData('text/html'));
|
return Array.from(clipboardData.types).includes('text/html') && (/<a/i).test(clipboardData.getData('text/html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlainText(clipboardData: DataTransfer): string | boolean {
|
|
||||||
if (Array.from(clipboardData.types).indexOf('text/plain') === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plainText = clipboardData.getData('text/plain');
|
|
||||||
|
|
||||||
return plainText;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isGitHubCodeBlock(tableClassName: string): boolean {
|
export function isGitHubCodeBlock(tableClassName: string): boolean {
|
||||||
const result = (/\b(js|blob|diff)-./).test(tableClassName);
|
const result = (/\b(js|blob|diff)-./).test(tableClassName);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHttpProtocol(url: string): boolean {
|
export function isTextUrl(clipboardData: DataTransfer): boolean {
|
||||||
return url.startsWith('http://');
|
const clipboardText = clipboardData.getData('text/plain');
|
||||||
|
return clipboardText.startsWith('http://') || clipboardText.startsWith('https://');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHttpsProtocol(url: string): boolean {
|
function isTableWithoutHeaderRow(table: HTMLTableElement): boolean {
|
||||||
return url.startsWith('https://');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHeaderlessTable(table: HTMLTableElement): boolean {
|
|
||||||
return table.querySelectorAll('th').length === 0;
|
return table.querySelectorAll('th').length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMarkdownMessage(clipboardData: DataTransfer, message?: string, caretPosition?: number): string {
|
/**
|
||||||
|
* Formats the given HTML clipboard data into a Markdown message.
|
||||||
|
* @returns {Object} An object containing 'formattedMessage' and 'formattedMarkdown'.
|
||||||
|
* @property {string} formattedMessage - The formatted message, including the formatted Markdown.
|
||||||
|
* @property {string} formattedMarkdown - The resulting Markdown from the HTML clipboard data.
|
||||||
|
*/
|
||||||
|
export function formatMarkdownMessage(clipboardData: DataTransfer, message?: string, caretPosition?: number): {formattedMessage: string; formattedMarkdown: string} {
|
||||||
const html = clipboardData.getData('text/html');
|
const html = clipboardData.getData('text/html');
|
||||||
|
|
||||||
//TODO@michel: Instantiate turndown service in a central file instead
|
let formattedMarkdown = turndownService.turndown(html).trim();
|
||||||
const service = new TurndownService({emDelimiter: '*'}).remove('style');
|
|
||||||
service.use(tables);
|
|
||||||
let markdownFormattedMessage = service.turndown(html).trim();
|
|
||||||
|
|
||||||
const table = getTable(clipboardData);
|
const table = getHtmlTable(clipboardData);
|
||||||
|
if (table && isTableWithoutHeaderRow(table)) {
|
||||||
if (table && isHeaderlessTable(table)) {
|
const newLineLimiter = '\n';
|
||||||
markdownFormattedMessage += '\n';
|
formattedMarkdown = `${formattedMarkdown}${newLineLimiter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let formattedMessage: string;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return markdownFormattedMessage;
|
formattedMessage = formattedMarkdown;
|
||||||
|
} else if (typeof caretPosition === 'undefined') {
|
||||||
|
formattedMessage = `${message}\n\n${formattedMarkdown}`;
|
||||||
|
} else {
|
||||||
|
const newMessage = [message.slice(0, caretPosition) + '\n', formattedMarkdown, message.slice(caretPosition)];
|
||||||
|
formattedMessage = newMessage.join('');
|
||||||
}
|
}
|
||||||
if (typeof caretPosition === 'undefined') {
|
|
||||||
return `${message}\n\n${markdownFormattedMessage}`;
|
return {formattedMessage, formattedMarkdown};
|
||||||
}
|
|
||||||
const newMessage = [message.slice(0, caretPosition) + '\n', markdownFormattedMessage, message.slice(caretPosition)];
|
|
||||||
return newMessage.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatGithubCodePaste({message, clipboardData, selectionStart, selectionEnd}: FormatCodeOptions): {formattedMessage: string; formattedCodeBlock: string} {
|
/**
|
||||||
const textSelected = selectionStart !== selectionEnd;
|
* Format the incoming github code paste into a markdown code block.
|
||||||
const {firstPiece, lastPiece} = textSelected ? splitMessageBasedOnTextSelection(selectionStart ?? message.length, selectionEnd ?? message.length, message) : splitMessageBasedOnCaretPosition(selectionStart ?? message.length, message);
|
* This function assumes that the clipboardData contains a code block.
|
||||||
|
* @returns {Object} An object containing the 'formattedMessage' and 'formattedCodeBlock'.
|
||||||
|
* @property {string} formattedMessage - The complete formatted message including the code block.
|
||||||
|
* @property {string} formattedCodeBlock - The resulting code block from the clipboard data.
|
||||||
|
*/
|
||||||
|
export function formatGithubCodePaste({message, clipboardData, selectionStart, selectionEnd}: FormatMarkdownParams): {formattedMessage: string; formattedCodeBlock: string} {
|
||||||
|
const isTextSelected = selectionStart !== selectionEnd;
|
||||||
|
const {firstPiece, lastPiece} = isTextSelected ? splitMessageBasedOnTextSelection(selectionStart ?? message.length, selectionEnd ?? message.length, message) : splitMessageBasedOnCaretPosition(selectionStart ?? message.length, message);
|
||||||
|
|
||||||
// Add new lines if content exists before or after the cursor.
|
// Add new lines if content exists before or after the cursor.
|
||||||
const requireStartLF = firstPiece === '' ? '' : '\n';
|
const requireStartLF = firstPiece === '' ? '' : '\n';
|
||||||
const requireEndLF = lastPiece === '' ? '' : '\n';
|
const requireEndLF = lastPiece === '' ? '' : '\n';
|
||||||
const formattedCodeBlock = requireStartLF + '```\n' + getPlainText(clipboardData) + '\n```' + requireEndLF;
|
const clipboardText = clipboardData.getData('text/plain');
|
||||||
|
const formattedCodeBlock = requireStartLF + '```\n' + clipboardText + '\n```' + requireEndLF;
|
||||||
const formattedMessage = `${firstPiece}${formattedCodeBlock}${lastPiece}`;
|
const formattedMessage = `${firstPiece}${formattedCodeBlock}${lastPiece}`;
|
||||||
|
|
||||||
return {formattedMessage, formattedCodeBlock};
|
return {formattedMessage, formattedCodeBlock};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the incoming link paste into a markdown link.
|
||||||
|
* This function assumes that the clipboardData contains a link.
|
||||||
|
* @returns The resulting markdown link from the clipboard data.
|
||||||
|
*/
|
||||||
|
export function formatMarkdownLinkMessage({message, clipboardData, selectionStart, selectionEnd}: FormatMarkdownParams) {
|
||||||
|
const isTextSelected = selectionStart !== selectionEnd;
|
||||||
|
|
||||||
|
let selectedText = '';
|
||||||
|
if (isTextSelected) {
|
||||||
|
selectedText = message.slice(selectionStart || 0, selectionEnd || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = clipboardData.getData('text/plain');
|
||||||
|
const markdownLink = `[${selectedText}](${url})`;
|
||||||
|
|
||||||
|
return markdownLink;
|
||||||
|
}
|
||||||
|
10
webapp/channels/src/utils/turndown.ts
Normal file
10
webapp/channels/src/utils/turndown.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import TurndownService from 'turndown';
|
||||||
|
import {tables} from '@guyplusplus/turndown-plugin-gfm';
|
||||||
|
|
||||||
|
const turndownService = new TurndownService({emDelimiter: '*'}).remove('style');
|
||||||
|
turndownService.use(tables);
|
||||||
|
|
||||||
|
export default turndownService;
|
Loading…
Reference in New Issue
Block a user