diff --git a/packages/markups/src/blocks/OrderedListBlock.js b/packages/markups/src/blocks/OrderedListBlock.js index 0c1a3c62f3..da404f9298 100644 --- a/packages/markups/src/blocks/OrderedListBlock.js +++ b/packages/markups/src/blocks/OrderedListBlock.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineElements from '../elements/InlineElements'; +import { listStyles } from './blocks.styles'; const OrderedListBlock = ({ items }) => ( -
    +
      {items.map((item, index) => (
    1. diff --git a/packages/markups/src/blocks/UnOrderedListBlock.js b/packages/markups/src/blocks/UnOrderedListBlock.js index a0f5f9ecf2..4ac0130beb 100644 --- a/packages/markups/src/blocks/UnOrderedListBlock.js +++ b/packages/markups/src/blocks/UnOrderedListBlock.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineElements from '../elements/InlineElements'; +import { listStyles } from './blocks.styles'; const UnOrderedListBlock = ({ items }) => ( -
        +
          {items.map((item, index) => (
        • diff --git a/packages/markups/src/blocks/blocks.styles.js b/packages/markups/src/blocks/blocks.styles.js index 63765585de..87d2c85dbb 100644 --- a/packages/markups/src/blocks/blocks.styles.js +++ b/packages/markups/src/blocks/blocks.styles.js @@ -9,3 +9,8 @@ export const TaskListBlockStyles = { gap: 0.5em; `, }; + +export const listStyles = css` + padding-left: 1.5em; + margin: 0; +`; diff --git a/packages/react/src/lib/insertListPrefix.js b/packages/react/src/lib/insertListPrefix.js new file mode 100644 index 0000000000..68f549aed4 --- /dev/null +++ b/packages/react/src/lib/insertListPrefix.js @@ -0,0 +1,20 @@ +const insertListPrefix = (messageRef, listPrefix) => { + const input = messageRef.current; + if (!input) return; + + const { selectionStart, value } = input; + const before = value.slice(0, selectionStart); + const after = value.slice(selectionStart); + + const needsNewline = before.length > 0 && !before.endsWith('\n'); + const prefix = listPrefix(1); + const insert = (needsNewline ? '\n' : '') + prefix; + + input.value = before + insert + after; + const cursorPos = selectionStart + insert.length; + input.selectionStart = cursorPos; + input.selectionEnd = cursorPos; + input.focus(); +}; + +export default insertListPrefix; diff --git a/packages/react/src/lib/textFormat.js b/packages/react/src/lib/textFormat.js index a1978c0033..c69d6da5cc 100644 --- a/packages/react/src/lib/textFormat.js +++ b/packages/react/src/lib/textFormat.js @@ -8,4 +8,16 @@ export const formatter = [ pattern: '```\n{{text}}\n```', tooltip: 'Multi-line code', }, + { + name: 'list-numbers', + type: 'list', + listPrefix: (n) => `${n}. `, + tooltip: 'Ordered list', + }, + { + name: 'list-bullets', + type: 'list', + listPrefix: () => '- ', + tooltip: 'Unordered list', + }, ]; diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index e753b689ae..2e43e86673 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -449,10 +449,51 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { formatSelection(messageRef, '*{{text}}*'); break; } - case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': + case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': { e.preventDefault(); - handleNewLine(e); + const input = messageRef.current; + const { value, selectionStart } = input; + const textBefore = value.slice(0, selectionStart); + const currentLine = textBefore.split('\n').pop(); + // Match numbered list: "1. ", "2. ", etc. + const olMatch = currentLine.match(/^(\d+)\.\s/); + // Match bullet list: "- " + const ulMatch = currentLine.match(/^-\s/); + + if (olMatch && currentLine.trim() === `${olMatch[1]}.`) { + const lineStart = selectionStart - currentLine.length; + const rest = value.slice(selectionStart); + input.value = value.slice(0, lineStart) + rest; + input.selectionStart = lineStart; + input.selectionEnd = lineStart; + handleNewLine(e, false); + } else if (ulMatch && currentLine.trim() === '-') { + const lineStart = selectionStart - currentLine.length; + const rest = value.slice(selectionStart); + input.value = value.slice(0, lineStart) + rest; + input.selectionStart = lineStart; + input.selectionEnd = lineStart; + handleNewLine(e, false); + } else if (olMatch) { + const nextNum = parseInt(olMatch[1], 10) + 1; + const prefix = `\n${nextNum}. `; + const after = value.slice(selectionStart); + input.value = textBefore + prefix + after; + input.selectionStart = selectionStart + prefix.length; + input.selectionEnd = selectionStart + prefix.length; + handleNewLine(e, false); + } else if (ulMatch) { + const prefix = '\n- '; + const after = value.slice(selectionStart); + input.value = textBefore + prefix + after; + input.selectionStart = selectionStart + prefix.length; + input.selectionEnd = selectionStart + prefix.length; + handleNewLine(e, false); + } else { + handleNewLine(e); + } break; + } case e.code === 'Escape': if (editMessage.msg || editMessage.attachments) { e.preventDefault(); diff --git a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js index 5d8c20a600..dfa72f50a6 100644 --- a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js +++ b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js @@ -15,6 +15,7 @@ import AudioMessageRecorder from './AudioMessageRecorder'; import VideoMessageRecorder from './VideoMessageRecoder'; import { getChatInputFormattingToolbarStyles } from './ChatInput.styles'; import formatSelection from '../../lib/formatSelection'; +import insertListPrefix from '../../lib/insertListPrefix'; import InsertLinkToolBox from './InsertLinkToolBox'; const ChatInputFormattingToolbar = ({ @@ -23,7 +24,15 @@ const ChatInputFormattingToolbar = ({ triggerButton, optionConfig = { surfaceItems: ['emoji', 'formatter', 'link', 'audio', 'video', 'file'], - formatters: ['bold', 'italic', 'strike', 'code', 'multiline'], + formatters: [ + 'bold', + 'italic', + 'strike', + 'code', + 'multiline', + 'list-numbers', + 'list-bullets', + ], smallScreenSurfaceItems: ['emoji', 'video', 'audio', 'file'], popOverItems: ['formatter', 'link'], }, @@ -55,7 +64,11 @@ const ChatInputFormattingToolbar = ({ inputRef.current.click(); }; const handleFormatterClick = (item) => { - formatSelection(messageRef, item.pattern); + if (item.type === 'list') { + insertListPrefix(messageRef, item.listPrefix); + } else { + formatSelection(messageRef, item.pattern); + } setPopoverOpen(false); }; const handleEmojiClick = (emojiEvent) => { @@ -224,7 +237,7 @@ const ChatInputFormattingToolbar = ({ ghost onClick={() => { if (isRecordingMessage) return; - formatSelection(messageRef, item.pattern); + handleFormatterClick(item); }} > - formatSelection(messageRef, itemInFormatter.pattern) - } + onClick={() => handleFormatterClick(itemInFormatter)} > ( + + + +); + +export default ListBullets; diff --git a/packages/ui-elements/src/components/Icon/icons/ListNumbers.js b/packages/ui-elements/src/components/Icon/icons/ListNumbers.js new file mode 100644 index 0000000000..80ea33f4c5 --- /dev/null +++ b/packages/ui-elements/src/components/Icon/icons/ListNumbers.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const ListNumbers = (props) => ( + + + +); + +export default ListNumbers; diff --git a/packages/ui-elements/src/components/Icon/icons/index.js b/packages/ui-elements/src/components/Icon/icons/index.js index 1f416a020a..8d2b920731 100644 --- a/packages/ui-elements/src/components/Icon/icons/index.js +++ b/packages/ui-elements/src/components/Icon/icons/index.js @@ -65,6 +65,8 @@ import Arc from './Arc'; import Avatar from './Avatar'; import FormatText from './FormatText'; import Cog from './Cog'; +import ListNumbers from './ListNumbers'; +import ListBullets from './ListBullets'; import Team from './Team'; const icons = { @@ -136,6 +138,8 @@ const icons = { avatar: Avatar, 'format-text': FormatText, cog: Cog, + 'list-numbers': ListNumbers, + 'list-bullets': ListBullets, }; export default icons;