Update notebook styling
This commit is contained in:
283
packages/web/components/patterns/ArticleNotes.tsx
Normal file
283
packages/web/components/patterns/ArticleNotes.tsx
Normal file
@ -0,0 +1,283 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
import {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { formattedShortTime } from '../../lib/dateFormatting'
|
||||
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import MdEditor, { Plugins } from 'react-markdown-editor-lite'
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
|
||||
import { Button } from '../elements/Button'
|
||||
import {
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
ModalRoot,
|
||||
} from '../elements/ModalPrimitives'
|
||||
import { CloseButton } from '../elements/CloseButton'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import MDEditorSavePlugin from './MDEditorSavePlugin'
|
||||
import HandleFullScreen from './MDEditorSavePlugin'
|
||||
import Counter from './MDEditorSavePlugin'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { RcEditorStyles } from './RcEditorStyles'
|
||||
|
||||
const mdParser = new MarkdownIt()
|
||||
|
||||
MdEditor.use(Plugins.TabInsert, {
|
||||
tabMapValue: 1, // note that 1 means a '\t' instead of ' '.
|
||||
})
|
||||
|
||||
console.log()
|
||||
MdEditor.use(Counter)
|
||||
|
||||
type NoteSectionProps = {
|
||||
targetId: string
|
||||
|
||||
placeHolder: string
|
||||
mode: 'edit' | 'preview'
|
||||
|
||||
sizeMode: 'normal' | 'maximized'
|
||||
setEditMode: (set: 'edit' | 'preview') => void
|
||||
|
||||
text: string | undefined
|
||||
saveText: (text: string, completed: (success: boolean) => void) => void
|
||||
}
|
||||
|
||||
export function ArticleNotes(props: NoteSectionProps): JSX.Element {
|
||||
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
|
||||
|
||||
const saveText = useCallback(
|
||||
(text, updateTime) => {
|
||||
props.saveText(text, (success) => {
|
||||
if (success) {
|
||||
setLastSaved(updateTime)
|
||||
}
|
||||
})
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<MarkdownNote
|
||||
targetId={props.targetId}
|
||||
placeHolder={props.placeHolder}
|
||||
sizeMode={props.sizeMode}
|
||||
text={props.text}
|
||||
saveText={saveText}
|
||||
lastSaved={lastSaved}
|
||||
fillBackground={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type HighlightViewNoteProps = {
|
||||
targetId: string
|
||||
|
||||
placeHolder: string
|
||||
mode: 'edit' | 'preview'
|
||||
|
||||
highlight: Highlight
|
||||
|
||||
sizeMode: 'normal' | 'maximized'
|
||||
setEditMode: (set: 'edit' | 'preview') => void
|
||||
|
||||
text: string | undefined
|
||||
updateHighlight: (highlight: Highlight) => void
|
||||
}
|
||||
|
||||
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
||||
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
|
||||
|
||||
const saveText = useCallback(
|
||||
(text, updateTime) => {
|
||||
;(async () => {
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
highlightId: props.highlight?.id,
|
||||
})
|
||||
if (success) {
|
||||
setLastSaved(updateTime)
|
||||
props.highlight.annotation = text
|
||||
props.updateHighlight(props.highlight)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<MarkdownNote
|
||||
targetId={props.targetId}
|
||||
placeHolder={props.placeHolder}
|
||||
sizeMode={props.sizeMode}
|
||||
text={props.text}
|
||||
saveText={saveText}
|
||||
lastSaved={lastSaved}
|
||||
fillBackground={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type MarkdownNote = {
|
||||
targetId: string
|
||||
|
||||
placeHolder: string
|
||||
|
||||
sizeMode: 'normal' | 'maximized'
|
||||
|
||||
text: string | undefined
|
||||
fillBackground: boolean | undefined
|
||||
|
||||
lastSaved: Date | undefined
|
||||
saveText: (text: string, updateTime: Date) => void
|
||||
}
|
||||
|
||||
export function MarkdownNote(props: MarkdownNote): JSX.Element {
|
||||
const editorRef = useRef<MdEditor | null>(null)
|
||||
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
|
||||
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
|
||||
const isDark = isDarkTheme()
|
||||
|
||||
const saveRef = useRef(props.saveText)
|
||||
|
||||
useEffect(() => {
|
||||
saveRef.current = props.saveText
|
||||
}, [props.lastSaved, lastChanged])
|
||||
|
||||
const debouncedSave = useMemo<
|
||||
(text: string, updateTime: Date) => void
|
||||
>(() => {
|
||||
const func = (text: string, updateTime: Date) => {
|
||||
saveRef.current?.(text, updateTime)
|
||||
}
|
||||
return throttle(func, 3000)
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(
|
||||
data: { text: string; html: string },
|
||||
event?: ChangeEvent<HTMLTextAreaElement> | undefined
|
||||
) => {
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const updateTime = new Date()
|
||||
setLastChanged(updateTime)
|
||||
localStorage.setItem(`note-${props.targetId}`, JSON.stringify(data))
|
||||
debouncedSave(data.text, updateTime)
|
||||
},
|
||||
[props.lastSaved, lastChanged]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const saveMarkdownNote = () => {
|
||||
const md = editorRef.current?.getMdValue()
|
||||
if (md) {
|
||||
props.saveText(md, new Date())
|
||||
}
|
||||
}
|
||||
document.addEventListener('saveMarkdownNote', saveMarkdownNote)
|
||||
return () => {
|
||||
document.removeEventListener('saveMarkdownNote', saveMarkdownNote)
|
||||
}
|
||||
}, [props, editorRef])
|
||||
|
||||
console.log('isDark: ', isDark)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
...RcEditorStyles(isDark, true),
|
||||
pr: '25px',
|
||||
}}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code.toLowerCase() === 'escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdEditor
|
||||
key="note-editor"
|
||||
ref={editorRef}
|
||||
autoFocus={true}
|
||||
defaultValue={props.text}
|
||||
placeholder={props.placeHolder}
|
||||
view={{ menu: true, md: true, html: false }}
|
||||
canView={{
|
||||
menu: true,
|
||||
md: true,
|
||||
html: true,
|
||||
both: false,
|
||||
fullScreen: false,
|
||||
hideMenu: false,
|
||||
}}
|
||||
plugins={[
|
||||
'tab-insert',
|
||||
'header',
|
||||
'font-bold',
|
||||
'font-italic',
|
||||
'font-underline',
|
||||
'font-strikethrough',
|
||||
'list-unordered',
|
||||
'list-ordered',
|
||||
'block-quote',
|
||||
'link',
|
||||
'auto-resize',
|
||||
'save',
|
||||
]}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '180px',
|
||||
}}
|
||||
renderHTML={(text: string) => mdParser.render(text)}
|
||||
onChange={handleEditorChange}
|
||||
/>
|
||||
<HStack
|
||||
css={{
|
||||
minHeight: '15px',
|
||||
width: '100%',
|
||||
fontSize: '9px',
|
||||
mt: '5px',
|
||||
color: '$thTextSubtle',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
{errorSaving && (
|
||||
<SpanBox
|
||||
css={{
|
||||
width: '100%',
|
||||
fontSize: '9px',
|
||||
mt: '5px',
|
||||
}}
|
||||
>
|
||||
{errorSaving}
|
||||
</SpanBox>
|
||||
)}
|
||||
{props.lastSaved !== undefined ? (
|
||||
<>
|
||||
{lastChanged === props.lastSaved
|
||||
? 'Saved'
|
||||
: `Last saved ${formattedShortTime(
|
||||
props.lastSaved.toISOString()
|
||||
)}`}
|
||||
</>
|
||||
) : null}
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
33
packages/web/components/patterns/MDEditorSavePlugin.tsx
Normal file
33
packages/web/components/patterns/MDEditorSavePlugin.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Disc, FloppyDisk } from 'phosphor-react'
|
||||
import { PluginComponent, PluginProps } from 'react-markdown-editor-lite'
|
||||
import { SpanBox } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
|
||||
export default class MDEditorSavePlugin extends PluginComponent {
|
||||
static pluginName = 'save'
|
||||
|
||||
static align = 'right'
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
private handleClick() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ display: 'flex', pr: '5px' }}
|
||||
onClick={(event) => {
|
||||
document.dispatchEvent(new Event('saveMarkdownNote'))
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<FloppyDisk size={18} weight="bold" color="#757575" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
34
packages/web/components/patterns/RcEditorStyles.tsx
Normal file
34
packages/web/components/patterns/RcEditorStyles.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
export const RcEditorStyles = (isDark: boolean, shadow: boolean) => {
|
||||
return {
|
||||
'.rc-md-editor .rc-md-navigation': {
|
||||
background: 'var(--colors-grayBg)',
|
||||
borderBottom: '1px solid $thBorderSubtle',
|
||||
},
|
||||
'.rc-md-editor': {
|
||||
borderRadius: '5px',
|
||||
backgroundColor: isDark ? '#2A2A2A' : 'white',
|
||||
border: '1px solid $thBorderSubtle',
|
||||
boxShadow: shadow ? '0px 4px 4px rgba(33, 33, 33, 0.1)' : 'unset',
|
||||
},
|
||||
'.rc-md-navigation': {
|
||||
borderRadius: '5px',
|
||||
borderBottomLeftRadius: '0px',
|
||||
borderBottomRightRadius: '0px',
|
||||
background: 'var(--colors-grayBg)',
|
||||
},
|
||||
'.rc-md-editor .editor-container >.section': {
|
||||
borderRight: 'unset',
|
||||
},
|
||||
'.rc-md-editor .editor-container .sec-md .input': {
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '16px',
|
||||
color: isDark ? '#EBEBEB' : 'black',
|
||||
backgroundColor: isDark ? '#2A2A2A' : 'white',
|
||||
},
|
||||
'.rc-md-editor .drop-wrap': {
|
||||
border: '1px solid $thBorderSubtle',
|
||||
backgroundColor: isDark ? '#2A2A2A' : 'white',
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItems
|
||||
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { ArticleNoteBox } from '../../patterns/ArticleNotes'
|
||||
import { ArticleNotes } from '../../patterns/ArticleNotes'
|
||||
|
||||
type NotebookProps = {
|
||||
viewer: UserBasicData
|
||||
@ -328,7 +328,7 @@ export function Notebook(props: NotebookProps): JSX.Element {
|
||||
distribution="start"
|
||||
css={{ width: '100%', mt: '10px', gap: '10px' }}
|
||||
>
|
||||
<ArticleNoteBox
|
||||
<ArticleNotes
|
||||
mode={notesEditMode}
|
||||
targetId={props.item.id}
|
||||
sizeMode={props.sizeMode}
|
||||
|
||||
Reference in New Issue
Block a user