Skip to content

Commit 8dbd623

Browse files
committed
WIP MarkdownToolbar extracted to seperate file
1 parent 8212ec3 commit 8dbd623

File tree

2 files changed

+161
-199
lines changed

2 files changed

+161
-199
lines changed

components/MarkdownToolbar.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React from 'react'
2+
import IconButton from './theme/IconButton'
3+
import {
4+
HeadingIcon,
5+
BoldIcon,
6+
ItalicIcon,
7+
QuoteIcon,
8+
CodeIcon,
9+
LinkIcon,
10+
ListUnorderedIcon,
11+
ListOrderedIcon,
12+
TasklistIcon
13+
// TypographyIcon,
14+
// ChevronDownIcon,
15+
// ChevronUpIcon
16+
} from '@primer/octicons-react'
17+
import { ButtonToolbar, ButtonToolbarProps } from 'react-bootstrap'
18+
19+
// TODO: Better name for utility
20+
const applyMarkdown = (
21+
inputRef: React.RefObject<HTMLTextAreaElement>,
22+
left: string,
23+
right: string = ''
24+
): void => {
25+
// TODO: add expand single select to word functionality and support for one sided and multiline operations
26+
if (!inputRef.current) return
27+
const { value, selectionStart, selectionEnd } = inputRef.current
28+
29+
const selection = value.slice(selectionStart, selectionEnd)
30+
31+
const hasLeft =
32+
selectionStart >= left.length &&
33+
value.slice(selectionStart - left.length, selectionStart) === left
34+
35+
const hasRight =
36+
selectionEnd + right.length <= value.length &&
37+
value.slice(selectionEnd, selectionEnd + right.length) === right
38+
39+
inputRef.current.focus()
40+
41+
if (hasLeft && hasRight) {
42+
// Modification detected around selection, remove it!
43+
44+
// expand selection to include left and right
45+
inputRef.current.setSelectionRange(
46+
selectionStart - left.length,
47+
selectionEnd + right.length
48+
)
49+
// Replace with old selection
50+
document.execCommand('insertText', false, selection)
51+
52+
// update selection with removed left
53+
inputRef.current.setSelectionRange(
54+
selectionStart - left.length,
55+
selectionEnd - left.length
56+
)
57+
} else {
58+
// No modification detected, apply modifiers
59+
document.execCommand('insertText', false, left + selection + right)
60+
61+
/// update selection with added left
62+
inputRef.current.setSelectionRange(
63+
selectionStart + left.length,
64+
selectionEnd + left.length
65+
)
66+
}
67+
}
68+
69+
// TODO: Add hotkey support!
70+
const buttons = [
71+
{
72+
tooltipTitle: 'Add header text',
73+
togglerParams: ['### ', ''],
74+
Icon: HeadingIcon
75+
},
76+
{
77+
tooltipTitle: 'Add bold text <ctrl+b>',
78+
togglerParams: ['**', '**'],
79+
Icon: BoldIcon
80+
},
81+
{
82+
tooltipTitle: 'Add italic text <ctrl+i>',
83+
togglerParams: ['_', '_'],
84+
Icon: ItalicIcon
85+
},
86+
{
87+
tooltipTitle: 'Insert a quote',
88+
togglerParams: ['\n> ', ''],
89+
Icon: QuoteIcon
90+
},
91+
{
92+
tooltipTitle: 'Insert code <ctrl+e>',
93+
togglerParams: ['`', '`'],
94+
Icon: CodeIcon
95+
},
96+
{
97+
tooltipTitle: 'Add a link <ctrl+k>',
98+
togglerParams: ['[', '](URL)'],
99+
Icon: LinkIcon
100+
},
101+
{
102+
tooltipTitle: 'Add a bulleted list',
103+
togglerParams: ['\n- ', ''],
104+
Icon: ListUnorderedIcon
105+
},
106+
{
107+
tooltipTitle: 'Add a numbered list',
108+
togglerParams: ['\n1. ', ''],
109+
Icon: ListOrderedIcon
110+
},
111+
{
112+
tooltipTitle: 'Add a task list',
113+
togglerParams: ['\n- [] ', ''],
114+
Icon: TasklistIcon
115+
}
116+
]
117+
118+
type Props = {
119+
inputRef: React.RefObject<HTMLTextAreaElement>
120+
}
121+
122+
const MarkdownToolbar: React.FC<Props & ButtonToolbarProps> = ({
123+
inputRef,
124+
...buttonProps
125+
}) => {
126+
return (
127+
<ButtonToolbar {...buttonProps}>
128+
{buttons.map(({ tooltipTitle, togglerParams, Icon }, i) => (
129+
<IconButton
130+
key={i}
131+
className={i === 2 || i === 5 ? 'mr-3' : ''}
132+
tabIndex="-1"
133+
color="black"
134+
delay={{ show: 250 }}
135+
tooltipTitle={tooltipTitle}
136+
aria-label={tooltipTitle}
137+
placement="bottom-end"
138+
onClick={() =>
139+
applyMarkdown(inputRef, ...(togglerParams as [string, string]))
140+
}
141+
icon={<Icon size={'small'} />}
142+
/>
143+
))}
144+
</ButtonToolbar>
145+
)
146+
}
147+
148+
export default MarkdownToolbar

components/MdInput.tsx

Lines changed: 13 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,10 @@ import React, { useState, useEffect, useRef } from 'react'
22
import Markdown from 'markdown-to-jsx'
33
import noop from '../helpers/noop'
44
import styles from '../scss/mdInput.module.scss'
5-
import {
6-
HeadingIcon,
7-
BoldIcon,
8-
ItalicIcon,
9-
QuoteIcon,
10-
CodeIcon,
11-
LinkIcon,
12-
ListUnorderedIcon,
13-
ListOrderedIcon,
14-
TasklistIcon,
15-
TypographyIcon,
16-
ChevronDownIcon,
17-
ChevronUpIcon
18-
} from '@primer/octicons-react'
19-
20-
import IconButton from './theme/IconButton'
215
import { Nav } from 'react-bootstrap'
226
import { useBreakpoint } from '../helpers/useBreakpoint'
7+
import MarkdownToolbar from './MarkdownToolbar'
8+
239
type MdInputProps = {
2410
onChange?: Function
2511
placeHolder?: string
@@ -75,190 +61,16 @@ export const MdInput: React.FC<MdInputProps> = ({
7561
return () => window.removeEventListener('mouseup', updateHeight, false)
7662
}, [])
7763

78-
const previewBtnColor = preview ? 'black' : 'lightgrey'
79-
const writeBtnColor = preview ? 'lightgrey' : 'black'
80-
81-
const onChangeWithAutoSize = e => {
82-
if (!height && textareaRef.current) autoSize(textareaRef.current)
83-
onChange(e.target.value)
84-
}
85-
86-
// TODO: Export markdownToolbar and associated helpers to separate file
87-
const markdownInputToggler = (
88-
textareaRef,
89-
left: string,
90-
right: string = ''
91-
): void => {
92-
// TODO: add expand single select to word functionality and support for one sided and multiline operations
93-
if (!textareaRef.current) return
94-
const { value, selectionStart, selectionEnd } = textareaRef.current
95-
96-
const selection = value.slice(selectionStart, selectionEnd)
97-
98-
const hasLeft =
99-
selectionStart >= left.length &&
100-
value.slice(selectionStart - left.length, selectionStart) === left
101-
const hasRight =
102-
selectionEnd + right.length <= value.length &&
103-
value.slice(selectionEnd, selectionEnd + right.length) === right
104-
console.log({
105-
selection,
106-
value,
107-
selectionEnd,
108-
selectionStart,
109-
left,
110-
hasLeft,
111-
right,
112-
hasRight
113-
})
64+
// TODO: Convert tabs to old styles
65+
// const previewBtnColor = preview ? 'black' : 'lightgrey'
66+
// const writeBtnColor = preview ? 'lightgrey' : 'black'
11467

115-
// const fakeEvent = { target: { value: 'remove!' } }
116-
textareaRef.current.focus()
117-
if (hasLeft && hasRight) {
118-
// expand selection to include left and right
119-
textareaRef.current.setSelectionRange(
120-
selectionStart - left.length,
121-
selectionEnd + right.length
122-
)
123-
// Replace with old selection
124-
document.execCommand('insertText', false, selection)
125-
126-
// update selection
127-
// expand selection to include left and right
128-
textareaRef.current.setSelectionRange(
129-
selectionStart - left.length,
130-
selectionEnd - left.length
131-
)
132-
} else {
133-
document.execCommand('insertText', false, left + selection + right)
134-
textareaRef.current.setSelectionRange(
135-
selectionStart + left.length,
136-
selectionEnd + left.length
137-
)
138-
// onChangeWithAutoSize({ target: { value: 'add!' } })
68+
const onChangeWithAutoSize: React.ChangeEventHandler<HTMLTextAreaElement> =
69+
e => {
70+
if (!height && textareaRef.current) autoSize(textareaRef.current)
71+
onChange(e.target.value)
13972
}
140-
}
141-
142-
const markdownToolbar = (
143-
<div className="ml-auto">
144-
{/* <IconButton
145-
tabIndex="-1"
146-
color="black"
147-
aria-label="Toggle text tools"
148-
aria-expanded="true"
149-
onClick={e => console.log("I'm clicked")}
150-
icon={
151-
<>
152-
<TypographyIcon size={'small'} />
153-
<ChevronDownIcon size={'small'} />
154-
</>
155-
}
156-
/> */}
157-
<IconButton
158-
tabIndex="-1"
159-
color="black"
160-
delay={{ show: 250 }}
161-
tooltipTitle="Add header text"
162-
aria-label="Add header text"
163-
role="button"
164-
placement="bottom-end"
165-
onClick={() => markdownInputToggler(textareaRef, '### ', '')}
166-
icon={<HeadingIcon size={'small'} />}
167-
/>
168-
<IconButton
169-
tabIndex="-1"
170-
color="black"
171-
delay={{ show: 250 }}
172-
tooltipTitle="Add bold text <ctrl+b>"
173-
aria-label="Add bold text <ctrl+b>"
174-
role="button"
175-
placement="bottom-end"
176-
onClick={() => markdownInputToggler(textareaRef, '**', '**')}
177-
icon={<BoldIcon size={'small'} />}
178-
/>
179-
<IconButton
180-
tabIndex="-1"
181-
color="black"
182-
delay={{ show: 250 }}
183-
tooltipTitle="Add italic text <ctrl+i>"
184-
aria-label="Add italic text <ctrl+i>"
185-
role="button"
186-
placement="bottom-end"
187-
onClick={() => {
188-
markdownInputToggler(textareaRef, '_', '_')
189-
}}
190-
icon={<ItalicIcon size={'small'} />}
191-
/>
192-
193-
<IconButton
194-
tabIndex="-1"
195-
color="black"
196-
delay={{ show: 250 }}
197-
tooltipTitle="Insert a quote"
198-
aria-label="Insert a quote"
199-
role="button"
200-
placement="bottom-end"
201-
onClick={() => markdownInputToggler(textareaRef, '\n> ', '')}
202-
icon={<QuoteIcon size={'small'} />}
203-
/>
204-
<IconButton
205-
tabIndex="-1"
206-
color="black"
207-
delay={{ show: 250 }}
208-
tooltipTitle="Insert code <ctrl+e>"
209-
aria-label="Insert code <ctrl+e>"
210-
role="button"
211-
placement="bottom-end"
212-
onClick={() => markdownInputToggler(textareaRef, '`', '`')}
213-
icon={<CodeIcon size={'small'} />}
214-
/>
215-
<IconButton
216-
tabIndex="-1"
217-
color="black"
218-
delay={{ show: 250 }}
219-
tooltipTitle="Add a link <ctrl+k>"
220-
aria-label="Add a link <ctrl+k>"
221-
role="button"
222-
placement="bottom-end"
223-
onClick={() => markdownInputToggler(textareaRef, '[', '](URL)')}
224-
icon={<LinkIcon size={'small'} />}
225-
/>
22673

227-
<IconButton
228-
tabIndex="-1"
229-
color="black"
230-
delay={{ show: 250 }}
231-
tooltipTitle="Add a bulleted list"
232-
aria-label="Add a bulleted list"
233-
role="button"
234-
placement="bottom-end"
235-
onClick={() => markdownInputToggler(textareaRef, '\n- ', '')}
236-
icon={<ListUnorderedIcon size={'small'} />}
237-
/>
238-
<IconButton
239-
tabIndex="-1"
240-
color="black"
241-
delay={{ show: 250 }}
242-
tooltipTitle="Add a numbered list"
243-
aria-label="Add a numbered list"
244-
role="button"
245-
placement="bottom-end"
246-
onClick={() => markdownInputToggler(textareaRef, '\n1. ', '')}
247-
icon={<ListOrderedIcon size={'small'} />}
248-
/>
249-
<IconButton
250-
tabIndex="-1"
251-
color="black"
252-
delay={{ show: 250 }}
253-
tooltipTitle="Add a task list"
254-
aria-label="Add a task list"
255-
role="button"
256-
placement="bottom-end"
257-
onClick={() => markdownInputToggler(textareaRef, '\n- [] ', '')}
258-
icon={<TasklistIcon size={'small'} />}
259-
/>
260-
</div>
261-
)
26274
return (
26375
<div style={{ backgroundColor: bgColor }}>
26476
<Nav
@@ -273,9 +85,11 @@ export const MdInput: React.FC<MdInputProps> = ({
27385
<Nav.Item>
27486
<Nav.Link eventKey="Preview">Preview</Nav.Link>
27587
</Nav.Item>
276-
{!lessThanMd && !preview && markdownToolbar}
88+
{!lessThanMd && !preview && (
89+
<MarkdownToolbar inputRef={textareaRef} className="ml-auto" />
90+
)}
27791
</Nav>
278-
{lessThanMd && !preview && markdownToolbar}
92+
{lessThanMd && !preview && <MarkdownToolbar inputRef={textareaRef} />}
27993
{preview && (
28094
<>
28195
<Markdown

0 commit comments

Comments
 (0)