Skip to content

Commit 5b7dba3

Browse files
feat: add link input modal (#122)
* feat: add link input modal * fix: lint * refactor: remove unnecessary useCallback
1 parent 64a013f commit 5b7dba3

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"title": "Insert Link",
3+
"display": "Link Display Text",
4+
"displayPlaceholder": "Enter link display text",
5+
"linkTitle": "Link Description",
6+
"optional": "(Optional)",
7+
"titlePlaceholder": "Enter a description for the link target",
8+
"url": "Link URL",
9+
"urlPlaceholder": "Enter the URL to open when clicking the link",
10+
"openMethod": "Link Opening Method",
11+
"newTab": "Open in new tab",
12+
"currentTab": "Open in current window",
13+
"cancel": "Cancel",
14+
"confirm": "Confirm"
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"title": "插入文字連結",
3+
"display": "文字連結標題",
4+
"displayPlaceholder": "請輸入文字連結標題",
5+
"linkTitle": "文字連結說明",
6+
"optional": "(選填)",
7+
"titlePlaceholder": "請輸入文字連結導向目標的描述",
8+
"url": "文字連結 URL",
9+
"urlPlaceholder": "請輸入點擊文字連結後打開的網址",
10+
"openMethod": "連結開啟方式",
11+
"newTab": "在新分頁中開啟",
12+
"currentTab": "在當前視窗中開啟",
13+
"cancel": "取消",
14+
"confirm": "確認"
15+
}

src/components/edit-icons-tab.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import markdowns from '@/lib/tabs/markdowns';
99
import { compare } from '@/lib/data-process';
1010
import mathTabList from '@/lib/tabs/math';
1111
import ImageUploadModal from './image-upload-modal';
12+
import LinkInputModal from './link-input-modal';
1213
import Tooltip from './core/tooltip';
1314

1415
const generateUniqueId = (length = 8) => {
@@ -22,9 +23,21 @@ const EditIconsTab = ({ insertLatex, addImageToExport }) => {
2223
const [selectedMainTabIndex, setSelectedMainTabIndex] = useState(0);
2324
const [selectedMathTabIndex, setSelectedMathTabIndex] = useState(0);
2425
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
26+
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
2527

2628
const t = useTranslation('tabs');
2729

30+
const handleLinkConfirm = useCallback(
31+
(markdown) => {
32+
insertLatex({
33+
id: 'create_link',
34+
latex: markdown,
35+
offset: 0,
36+
});
37+
},
38+
[insertLatex]
39+
);
40+
2841
const handleImageConfirm = useCallback(
2942
(file, altText) => {
3043
const fileID = generateUniqueId();
@@ -129,6 +142,10 @@ const EditIconsTab = ({ insertLatex, addImageToExport }) => {
129142
setIsImageModalOpen(true);
130143
return;
131144
}
145+
if (tab.id === 'create_link') {
146+
setIsLinkModalOpen(true);
147+
return;
148+
}
132149
insertLatex(tab);
133150
}}
134151
>
@@ -144,6 +161,11 @@ const EditIconsTab = ({ insertLatex, addImageToExport }) => {
144161
onClose={() => setIsImageModalOpen(false)}
145162
onConfirm={handleImageConfirm}
146163
/>
164+
<LinkInputModal
165+
isOpen={isLinkModalOpen}
166+
onClose={() => setIsLinkModalOpen(false)}
167+
onConfirm={handleLinkConfirm}
168+
/>
147169
</>
148170
);
149171
};

src/components/link-input-modal.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useTranslation } from '@/lib/i18n';
4+
import BasicModal from '@/components/core/modal/basic-modal';
5+
6+
const LinkInputModal = ({ isOpen, onClose, onConfirm }) => {
7+
const [display, setDisplay] = useState('');
8+
const [title, setTitle] = useState('');
9+
const [url, setUrl] = useState('');
10+
const [openInNewTab, setOpenInNewTab] = useState(true);
11+
const t = useTranslation('link-input-modal');
12+
13+
const resetForm = () => {
14+
setDisplay('');
15+
setTitle('');
16+
setUrl('');
17+
setOpenInNewTab(true);
18+
};
19+
20+
const handleClose = () => {
21+
resetForm();
22+
onClose();
23+
};
24+
25+
const handleConfirm = () => {
26+
if (!display.trim() || !url.trim()) return;
27+
const prefix = openInNewTab ? '@' : '';
28+
const titlePart = title.trim() ? `[[${title.trim()}]]` : '';
29+
const markdown = `${prefix}[${display.trim()}]${titlePart}(${url.trim()})`;
30+
onConfirm(markdown);
31+
handleClose();
32+
};
33+
34+
return (
35+
<BasicModal
36+
title={t('title')}
37+
isOpen={isOpen}
38+
onClose={handleClose}
39+
onCancel={handleClose}
40+
onConfirm={handleConfirm}
41+
cancelLabel={t('cancel')}
42+
confirmLabel={t('confirm')}
43+
>
44+
<div className="flex flex-col gap-6">
45+
<div className="flex flex-col gap-2">
46+
<label className="text-text-primary text-base" htmlFor="link-display">
47+
{t('display')}
48+
</label>
49+
<input
50+
id="link-display"
51+
type="text"
52+
value={display}
53+
onChange={(e) => setDisplay(e.target.value)}
54+
placeholder={t('displayPlaceholder')}
55+
className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary"
56+
aria-required="true"
57+
/>
58+
</div>
59+
<div className="flex flex-col gap-2">
60+
<label
61+
className="flex gap-2 items-center text-text-primary text-base"
62+
htmlFor="link-title"
63+
>
64+
<span>{t('linkTitle')}</span>
65+
<span className="text-text-primary">{t('optional')}</span>
66+
</label>
67+
<input
68+
id="link-title"
69+
type="text"
70+
value={title}
71+
onChange={(e) => setTitle(e.target.value)}
72+
placeholder={t('titlePlaceholder')}
73+
className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary"
74+
/>
75+
</div>
76+
<div className="flex flex-col gap-2">
77+
<label className="text-text-primary text-base" htmlFor="link-url">
78+
{t('url')}
79+
</label>
80+
<input
81+
id="link-url"
82+
type="text"
83+
value={url}
84+
onChange={(e) => setUrl(e.target.value)}
85+
placeholder={t('urlPlaceholder')}
86+
className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary"
87+
aria-required="true"
88+
/>
89+
</div>
90+
<div className="flex flex-col gap-2">
91+
<span className="text-text-primary text-base">{t('openMethod')}</span>
92+
<div className="flex items-center">
93+
<label className="flex flex-1 items-center gap-3 py-1 cursor-pointer">
94+
<input
95+
type="radio"
96+
name="link-open-method"
97+
checked={openInNewTab}
98+
onChange={() => setOpenInNewTab(true)}
99+
className="accent-primary w-5 h-5 shrink-0"
100+
/>
101+
<span className="text-base text-text-primary">{t('newTab')}</span>
102+
</label>
103+
<label className="flex flex-1 items-center gap-3 px-2 py-1 cursor-pointer">
104+
<input
105+
type="radio"
106+
name="link-open-method"
107+
checked={!openInNewTab}
108+
onChange={() => setOpenInNewTab(false)}
109+
className="accent-primary w-5 h-5 shrink-0"
110+
/>
111+
<span className="text-base text-text-primary">{t('currentTab')}</span>
112+
</label>
113+
</div>
114+
</div>
115+
</div>
116+
</BasicModal>
117+
);
118+
};
119+
120+
LinkInputModal.propTypes = {
121+
isOpen: PropTypes.bool.isRequired,
122+
onClose: PropTypes.func.isRequired,
123+
onConfirm: PropTypes.func.isRequired,
124+
};
125+
126+
export default LinkInputModal;

0 commit comments

Comments
 (0)