-
Notifications
You must be signed in to change notification settings - Fork 383
Update the favorite name input in Add/Edit favorite panel #1663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
0861c9d
Update the favorite name input in Add/Edit favorite panel
alisa911 4cbb235
Update address input in Add/Edit favorite panel
alisa911 3ad1360
Fix paddings between inputs
alisa911 2919908
Fix css
alisa911 8707761
Replace react-swipeable-views with swiper in PhotosModal
alisa911 c3b3f1f
Install tiptap
alisa911 2fec946
Update axios
alisa911 01f9f63
Fix console error
alisa911 36665be
Add new HTML editor
alisa911 9cdd045
Fix bug with favorite editing
alisa911 03e84b5
Always select favorite group
alisa911 d454500
Remove unused dependencies
alisa911 bfd61b1
Rename Editor to RichTextEditor
alisa911 5e1c790
Improve html utils
alisa911 8a4c6c6
Add comment
alisa911 a3431b5
Roll back comments
alisa911 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import React from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { Box, Divider, IconButton, Tooltip } from '@mui/material'; | ||
| import FormatBoldIcon from '@mui/icons-material/FormatBold'; | ||
| import FormatItalicIcon from '@mui/icons-material/FormatItalic'; | ||
| import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined'; | ||
| import StrikethroughSIcon from '@mui/icons-material/StrikethroughS'; | ||
| import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; | ||
| import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | ||
| import FormatQuoteIcon from '@mui/icons-material/FormatQuote'; | ||
| import LinkIcon from '@mui/icons-material/Link'; | ||
| import LinkOffIcon from '@mui/icons-material/LinkOff'; | ||
| import UndoIcon from '@mui/icons-material/Undo'; | ||
| import RedoIcon from '@mui/icons-material/Redo'; | ||
| import styles from './editor.module.css'; | ||
|
|
||
| export default function EditorToolbar({ editor }) { | ||
| const { t } = useTranslation(); | ||
| const canUndo = editor?.can().undo() ?? false; | ||
| const canRedo = editor?.can().redo() ?? false; | ||
|
|
||
| return ( | ||
| <Box className={styles.toolbar}> | ||
| <ToolbarBtn | ||
| title={t('web:editor_undo')} | ||
| icon={<UndoIcon fontSize="small" />} | ||
| disabled={!canUndo} | ||
| onClick={() => editor?.chain().focus().undo().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_redo')} | ||
| icon={<RedoIcon fontSize="small" />} | ||
| disabled={!canRedo} | ||
| onClick={() => editor?.chain().focus().redo().run()} | ||
| /> | ||
|
|
||
| <Divider orientation="vertical" flexItem className={styles.toolbarDivider} /> | ||
|
|
||
| <HeadingBtn editor={editor} level={1} label="H1" title={t('web:editor_heading_h1')} /> | ||
| <HeadingBtn editor={editor} level={2} label="H2" title={t('web:editor_heading_h2')} /> | ||
| <HeadingBtn editor={editor} level={3} label="H3" title={t('web:editor_heading_h3')} /> | ||
|
|
||
| <Divider orientation="vertical" flexItem className={styles.toolbarDivider} /> | ||
|
|
||
| <ToolbarBtn | ||
| title={t('web:editor_bullet_list')} | ||
| icon={<FormatListBulletedIcon fontSize="small" />} | ||
| active={editor?.isActive('bulletList') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleBulletList().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_ordered_list')} | ||
| icon={<FormatListNumberedIcon fontSize="small" />} | ||
| active={editor?.isActive('orderedList') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleOrderedList().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_blockquote')} | ||
| icon={<FormatQuoteIcon fontSize="small" />} | ||
| active={editor?.isActive('blockquote') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleBlockquote().run()} | ||
| /> | ||
|
|
||
| <Divider orientation="vertical" flexItem className={styles.toolbarDivider} /> | ||
|
|
||
| <ToolbarBtn | ||
| title={t('web:editor_bold')} | ||
| icon={<FormatBoldIcon fontSize="small" />} | ||
| active={editor?.isActive('bold') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleBold().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_italic')} | ||
| icon={<FormatItalicIcon fontSize="small" />} | ||
| active={editor?.isActive('italic') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleItalic().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_underline')} | ||
| icon={<FormatUnderlinedIcon fontSize="small" />} | ||
| active={editor?.isActive('underline') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleUnderline().run()} | ||
| /> | ||
| <ToolbarBtn | ||
| title={t('web:editor_strikethrough')} | ||
| icon={<StrikethroughSIcon fontSize="small" />} | ||
| active={editor?.isActive('strike') ?? false} | ||
| onClick={() => editor?.chain().focus().toggleStrike().run()} | ||
| /> | ||
|
|
||
| <Divider orientation="vertical" flexItem className={styles.toolbarDivider} /> | ||
|
|
||
| <LinkButton editor={editor} /> | ||
| </Box> | ||
| ); | ||
| } | ||
|
|
||
| function ToolbarBtn({ title, icon, active, onClick, disabled }) { | ||
| return ( | ||
| <Tooltip title={title} arrow> | ||
| <span> | ||
| <IconButton | ||
| size="small" | ||
| disabled={disabled} | ||
| className={active ? styles.toolbarBtnActive : styles.toolbarBtn} | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| onClick(); | ||
| }} | ||
| > | ||
| {icon} | ||
| </IconButton> | ||
| </span> | ||
| </Tooltip> | ||
| ); | ||
| } | ||
|
|
||
| function HeadingBtn({ editor, level, label, title }) { | ||
| return ( | ||
| <ToolbarBtn | ||
| title={title} | ||
| icon={<span className={styles.headingBtnLabel}>{label}</span>} | ||
| active={editor?.isActive('heading', { level }) ?? false} | ||
| onClick={() => editor?.chain().focus().toggleHeading({ level }).run()} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function LinkButton({ editor }) { | ||
| const { t } = useTranslation(); | ||
|
|
||
| if (!editor) return null; | ||
|
|
||
| const isLink = editor.isActive('link'); | ||
|
|
||
| function handleClick() { | ||
| if (isLink) { | ||
| editor.chain().focus().unsetLink().run(); | ||
|
|
||
| return; | ||
| } | ||
| const url = window.prompt(t('web:editor_link_url_prompt')); | ||
| if (url) { | ||
| editor.chain().focus().setLink({ href: url }).run(); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <ToolbarBtn | ||
| title={isLink ? t('web:editor_remove_link') : t('web:editor_add_link')} | ||
| icon={isLink ? <LinkOffIcon fontSize="small" /> : <LinkIcon fontSize="small" />} | ||
| active={isLink} | ||
| onClick={handleClick} | ||
| /> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import React from 'react'; | ||
| import { Box } from '@mui/material'; | ||
| import { useEditor, EditorContent } from '@tiptap/react'; | ||
| import StarterKit from '@tiptap/starter-kit'; | ||
| import EditorToolbar from './EditorToolbar'; | ||
| import styles from './editor.module.css'; | ||
|
|
||
| export default function RichTextEditor({ content, onChange, autofocus = true, editorId }) { | ||
| const editor = useEditor({ | ||
| extensions: [StarterKit.configure({ link: { openOnClick: false } })], | ||
| content, | ||
| autofocus, | ||
| onUpdate: ({ editor }) => onChange?.(editor.getHTML()), | ||
| editorProps: editorId ? { attributes: { id: editorId } } : undefined, | ||
| }); | ||
|
|
||
| return ( | ||
| <> | ||
| <EditorToolbar editor={editor} /> | ||
| <Box className={styles.editorContent}> | ||
| <EditorContent editor={editor} className={styles.prosemirror} /> | ||
| </Box> | ||
| </> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| .prosemirror :global(.ProseMirror) { | ||
| outline: none; | ||
| font-size: 16px; | ||
| line-height: 1.6; | ||
| color: var(--text-primary, #212121); | ||
| padding: 16px; | ||
| min-height: 120px; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror p) { | ||
| margin: 0 0 4px 0; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror p:last-child) { | ||
| margin-bottom: 0; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror h1) { | ||
| font-size: 20px; | ||
| font-weight: 700; | ||
| margin: 0 0 8px 0; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror h2) { | ||
| font-size: 17px; | ||
| font-weight: 600; | ||
| margin: 0 0 6px 0; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror h3) { | ||
| font-size: 15px; | ||
| font-weight: 600; | ||
| margin: 0 0 4px 0; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror ul), | ||
| .prosemirror :global(.ProseMirror ol) { | ||
| padding-left: 24px; | ||
| margin: 4px 0; | ||
| } | ||
|
|
||
| .editorContent { | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex-grow: 1; | ||
| min-height: 0; | ||
| overflow-y: auto; | ||
| scrollbar-gutter: stable; | ||
| } | ||
|
|
||
| .toolbar { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| height: 43px; | ||
| min-height: 43px; | ||
| border-bottom: 1px solid #e0e0e0; | ||
| gap: 0; | ||
| flex-shrink: 0; | ||
| } | ||
|
|
||
| .toolbarBtn { | ||
| color: var(--text-secondary, #666) !important; | ||
| border-radius: 4px !important; | ||
| padding: 2px !important; | ||
| } | ||
|
|
||
| .toolbarBtn:hover { | ||
| background-color: #f0f0f0 !important; | ||
| } | ||
|
|
||
| .toolbarBtnActive { | ||
| color: #237BFF !important; | ||
| background-color: #e9f0fb !important; | ||
| border-radius: 4px !important; | ||
| padding: 2px !important; | ||
| } | ||
|
|
||
| .toolbarDivider { | ||
| margin: 0 4px !important; | ||
| height: 20px !important; | ||
| align-self: center !important; | ||
| } | ||
|
|
||
| .headingBtnLabel { | ||
| font-size: 13px; | ||
| font-weight: 600; | ||
| line-height: 1; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror blockquote) { | ||
| border-left: 3px solid #ccc; | ||
| margin: 4px 0; | ||
| padding-left: 12px; | ||
| color: var(--text-secondary, #666); | ||
| font-style: italic; | ||
| } | ||
|
|
||
| .prosemirror :global(.ProseMirror a) { | ||
| color: #237BFF; | ||
| text-decoration: underline; | ||
| cursor: pointer; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /** | ||
| * Converts legacy plain text to HTML for loading into the rich text editor. | ||
| * Used only for backward compatibility with descriptions saved before rich text was introduced. | ||
| * | ||
| * - Already-HTML strings (starting with '<') are returned as-is. | ||
| * - Double (or more) newlines → paragraph breaks <p> | ||
| * - Single newlines within a paragraph → <br> | ||
| */ | ||
| export function textToHTML(text) { | ||
| if (!text) return ''; | ||
| if (text.trimStart().startsWith('<')) return text; | ||
|
|
||
| return text | ||
| .split(/\n{2,}/) | ||
| .map((para) => `<p>${para.replaceAll('\n', '<br>')}</p>`) | ||
| .join(''); | ||
| } | ||
|
|
||
| /** | ||
| * Strips HTML tags and converts block-level elements to newlines. | ||
| * Used to generate plain-text previews from rich text HTML. | ||
| * | ||
| * Handles: <p>, <h1–h6>, <li>, <blockquote>, <br> | ||
| */ | ||
| export function htmlToText(html) { | ||
| if (!html) return ''; | ||
|
|
||
| return html | ||
| .replaceAll(/<\/p>/gi, '\n') | ||
| .replaceAll(/<\/h[1-6]>/gi, '\n') | ||
| .replaceAll(/<\/li>/gi, '\n') | ||
| .replaceAll(/<\/blockquote>/gi, '\n') | ||
| .replaceAll(/<br\s*\/?>/gi, '\n') | ||
| .replaceAll(/<[^>]+>/g, '') | ||
| .replace(/\n+$/, '') | ||
| .trim(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns plain text with collapsed whitespace — useful for search and comparison. | ||
| */ | ||
| export function stripHtml(html) { | ||
| return htmlToText(html).replace(/\s+/g, ' ').trim(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is unworthy to place childish-like code in a separate module. Are you sure that this is enough to cover use cases?