@xignature/docx-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,589 @@
1
+ import {
2
+ useTranslation
3
+ } from "./chunk-OG6X5JJA.mjs";
4
+
5
+ // src/react/components/dialogs/HyperlinkDialog.tsx
6
+ import { useState, useEffect, useCallback, useRef } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var DIALOG_OVERLAY_STYLE = {
9
+ position: "fixed",
10
+ top: 0,
11
+ left: 0,
12
+ right: 0,
13
+ bottom: 0,
14
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
15
+ display: "flex",
16
+ alignItems: "center",
17
+ justifyContent: "center",
18
+ zIndex: 1e4
19
+ };
20
+ var DIALOG_CONTENT_STYLE = {
21
+ backgroundColor: "white",
22
+ borderRadius: "8px",
23
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)",
24
+ minWidth: "400px",
25
+ maxWidth: "500px",
26
+ width: "100%",
27
+ margin: "20px"
28
+ };
29
+ var DIALOG_HEADER_STYLE = {
30
+ display: "flex",
31
+ justifyContent: "space-between",
32
+ alignItems: "center",
33
+ padding: "16px 20px",
34
+ borderBottom: "1px solid var(--doc-border)"
35
+ };
36
+ var DIALOG_TITLE_STYLE = {
37
+ margin: 0,
38
+ fontSize: "18px",
39
+ fontWeight: 600,
40
+ color: "var(--doc-text)"
41
+ };
42
+ var CLOSE_BUTTON_STYLE = {
43
+ background: "none",
44
+ border: "none",
45
+ fontSize: "20px",
46
+ cursor: "pointer",
47
+ color: "var(--doc-text-muted)",
48
+ padding: "4px 8px",
49
+ lineHeight: 1
50
+ };
51
+ var DIALOG_BODY_STYLE = {
52
+ padding: "20px"
53
+ };
54
+ var FORM_GROUP_STYLE = {
55
+ marginBottom: "16px"
56
+ };
57
+ var LABEL_STYLE = {
58
+ display: "block",
59
+ marginBottom: "6px",
60
+ fontSize: "14px",
61
+ fontWeight: 500,
62
+ color: "var(--doc-text)"
63
+ };
64
+ var INPUT_STYLE = {
65
+ width: "100%",
66
+ padding: "10px 12px",
67
+ border: "1px solid var(--doc-border-input)",
68
+ borderRadius: "4px",
69
+ fontSize: "14px",
70
+ boxSizing: "border-box",
71
+ outline: "none"
72
+ };
73
+ var INPUT_ERROR_STYLE = {
74
+ ...INPUT_STYLE,
75
+ borderColor: "var(--doc-error)"
76
+ };
77
+ var SELECT_STYLE = {
78
+ ...INPUT_STYLE,
79
+ cursor: "pointer"
80
+ };
81
+ var ERROR_TEXT_STYLE = {
82
+ color: "var(--doc-error)",
83
+ fontSize: "12px",
84
+ marginTop: "4px"
85
+ };
86
+ var HINT_TEXT_STYLE = {
87
+ color: "var(--doc-text-muted)",
88
+ fontSize: "12px",
89
+ marginTop: "4px"
90
+ };
91
+ var TAB_CONTAINER_STYLE = {
92
+ display: "flex",
93
+ borderBottom: "1px solid var(--doc-border)",
94
+ marginBottom: "16px"
95
+ };
96
+ var TAB_BUTTON_STYLE = {
97
+ padding: "10px 16px",
98
+ border: "none",
99
+ background: "none",
100
+ cursor: "pointer",
101
+ fontSize: "14px",
102
+ color: "var(--doc-text-muted)",
103
+ borderBottom: "2px solid transparent",
104
+ marginBottom: "-1px"
105
+ };
106
+ var TAB_BUTTON_ACTIVE_STYLE = {
107
+ ...TAB_BUTTON_STYLE,
108
+ color: "var(--doc-link)",
109
+ borderBottomColor: "var(--doc-link)",
110
+ fontWeight: 500
111
+ };
112
+ var DIALOG_FOOTER_STYLE = {
113
+ display: "flex",
114
+ justifyContent: "flex-end",
115
+ gap: "12px",
116
+ padding: "16px 20px",
117
+ borderTop: "1px solid var(--doc-border)"
118
+ };
119
+ var BUTTON_BASE_STYLE = {
120
+ padding: "10px 20px",
121
+ borderRadius: "4px",
122
+ fontSize: "14px",
123
+ fontWeight: 500,
124
+ cursor: "pointer",
125
+ border: "none"
126
+ };
127
+ var PRIMARY_BUTTON_STYLE = {
128
+ ...BUTTON_BASE_STYLE,
129
+ backgroundColor: "var(--doc-link)",
130
+ color: "white"
131
+ };
132
+ var SECONDARY_BUTTON_STYLE = {
133
+ ...BUTTON_BASE_STYLE,
134
+ backgroundColor: "var(--doc-bg-hover)",
135
+ color: "var(--doc-text)",
136
+ border: "1px solid var(--doc-border-input)"
137
+ };
138
+ var DANGER_BUTTON_STYLE = {
139
+ ...BUTTON_BASE_STYLE,
140
+ backgroundColor: "var(--doc-error)",
141
+ color: "white"
142
+ };
143
+ var DISABLED_BUTTON_STYLE = {
144
+ ...BUTTON_BASE_STYLE,
145
+ backgroundColor: "var(--doc-border-input)",
146
+ color: "var(--doc-text-muted)",
147
+ cursor: "not-allowed"
148
+ };
149
+ function isValidUrl(url) {
150
+ if (!url || url.trim() === "") {
151
+ return false;
152
+ }
153
+ const trimmed = url.trim();
154
+ if (trimmed.startsWith("mailto:") || trimmed.startsWith("tel:")) {
155
+ return trimmed.length > 7;
156
+ }
157
+ if (trimmed.startsWith("ftp://")) {
158
+ return trimmed.length > 6;
159
+ }
160
+ try {
161
+ const urlToValidate = trimmed.match(/^https?:\/\//) ? trimmed : `https://${trimmed}`;
162
+ const parsed = new URL(urlToValidate);
163
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+ function normalizeUrl(url) {
169
+ if (!url) return "";
170
+ const trimmed = url.trim();
171
+ if (trimmed.startsWith("mailto:") || trimmed.startsWith("tel:") || trimmed.startsWith("ftp://")) {
172
+ return trimmed;
173
+ }
174
+ if (!trimmed.match(/^https?:\/\//)) {
175
+ return `https://${trimmed}`;
176
+ }
177
+ return trimmed;
178
+ }
179
+ function getUrlType(url) {
180
+ if (!url) return "unknown";
181
+ const trimmed = url.trim().toLowerCase();
182
+ if (trimmed.startsWith("mailto:")) return "email";
183
+ if (trimmed.startsWith("tel:")) return "phone";
184
+ if (trimmed.startsWith("ftp://")) return "ftp";
185
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return "web";
186
+ if (trimmed.includes("@") && !trimmed.includes(" ")) return "email";
187
+ return "web";
188
+ }
189
+ function HyperlinkDialog({
190
+ isOpen,
191
+ onClose,
192
+ onSubmit,
193
+ onRemove,
194
+ initialData,
195
+ selectedText = "",
196
+ isEditing = false,
197
+ bookmarks = [],
198
+ className,
199
+ style
200
+ }) {
201
+ const { t } = useTranslation();
202
+ const [linkType, setLinkType] = useState("url");
203
+ const [url, setUrl] = useState("");
204
+ const [displayText, setDisplayText] = useState("");
205
+ const [bookmark, setBookmark] = useState("");
206
+ const [tooltip, setTooltip] = useState("");
207
+ const [urlError, setUrlError] = useState("");
208
+ const [touched, setTouched] = useState(false);
209
+ const urlInputRef = useRef(null);
210
+ const bookmarkSelectRef = useRef(null);
211
+ useEffect(() => {
212
+ if (isOpen) {
213
+ if (initialData) {
214
+ if (initialData.bookmark) {
215
+ setLinkType("bookmark");
216
+ setBookmark(initialData.bookmark);
217
+ } else {
218
+ setLinkType("url");
219
+ setUrl(initialData.url || "");
220
+ }
221
+ setDisplayText(initialData.displayText || "");
222
+ setTooltip(initialData.tooltip || "");
223
+ } else {
224
+ setLinkType("url");
225
+ setUrl("");
226
+ setDisplayText(selectedText);
227
+ setBookmark("");
228
+ setTooltip("");
229
+ }
230
+ setUrlError("");
231
+ setTouched(false);
232
+ }
233
+ }, [isOpen, initialData, selectedText]);
234
+ useEffect(() => {
235
+ if (isOpen) {
236
+ setTimeout(() => {
237
+ if (linkType === "url") {
238
+ urlInputRef.current?.focus();
239
+ } else {
240
+ bookmarkSelectRef.current?.focus();
241
+ }
242
+ }, 100);
243
+ }
244
+ }, [isOpen, linkType]);
245
+ const validateUrl = useCallback(() => {
246
+ if (linkType === "url" && url.trim()) {
247
+ if (!isValidUrl(url)) {
248
+ setUrlError(t("dialogs.hyperlink.invalidUrl"));
249
+ } else {
250
+ setUrlError("");
251
+ }
252
+ } else {
253
+ setUrlError("");
254
+ }
255
+ }, [linkType, url, t]);
256
+ const handleSubmit = useCallback(
257
+ (e) => {
258
+ e?.preventDefault();
259
+ if (linkType === "url") {
260
+ if (!url.trim()) {
261
+ setUrlError(t("dialogs.hyperlink.urlRequired"));
262
+ setTouched(true);
263
+ return;
264
+ }
265
+ if (!isValidUrl(url)) {
266
+ setUrlError(t("dialogs.hyperlink.invalidUrl"));
267
+ setTouched(true);
268
+ return;
269
+ }
270
+ } else if (linkType === "bookmark") {
271
+ if (!bookmark) {
272
+ return;
273
+ }
274
+ }
275
+ const data = {
276
+ displayText: displayText.trim() || void 0,
277
+ tooltip: tooltip.trim() || void 0
278
+ };
279
+ if (linkType === "url") {
280
+ data.url = normalizeUrl(url);
281
+ } else {
282
+ data.bookmark = bookmark;
283
+ }
284
+ onSubmit(data);
285
+ },
286
+ [linkType, url, bookmark, displayText, tooltip, onSubmit]
287
+ );
288
+ const handleKeyDown = useCallback(
289
+ (e) => {
290
+ if (e.key === "Escape") {
291
+ onClose();
292
+ } else if (e.key === "Enter" && !e.shiftKey) {
293
+ const target = e.target;
294
+ if (target.tagName !== "TEXTAREA") {
295
+ e.preventDefault();
296
+ handleSubmit();
297
+ }
298
+ }
299
+ },
300
+ [onClose, handleSubmit]
301
+ );
302
+ const handleOverlayClick = useCallback(
303
+ (e) => {
304
+ if (e.target === e.currentTarget) {
305
+ onClose();
306
+ }
307
+ },
308
+ [onClose]
309
+ );
310
+ if (!isOpen) {
311
+ return null;
312
+ }
313
+ const hasBookmarks = bookmarks.length > 0;
314
+ const canSubmit = linkType === "url" && url.trim() && !urlError || linkType === "bookmark" && bookmark;
315
+ return /* @__PURE__ */ jsx(
316
+ "div",
317
+ {
318
+ className: `docx-hyperlink-dialog-overlay ${className || ""}`,
319
+ style: { ...DIALOG_OVERLAY_STYLE, ...style },
320
+ onClick: handleOverlayClick,
321
+ onKeyDown: handleKeyDown,
322
+ role: "dialog",
323
+ "aria-modal": "true",
324
+ "aria-labelledby": "hyperlink-dialog-title",
325
+ children: /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog", style: DIALOG_CONTENT_STYLE, children: [
326
+ /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-header", style: DIALOG_HEADER_STYLE, children: [
327
+ /* @__PURE__ */ jsx("h2", { id: "hyperlink-dialog-title", style: DIALOG_TITLE_STYLE, children: isEditing ? t("dialogs.hyperlink.titleEdit") : t("dialogs.hyperlink.titleInsert") }),
328
+ /* @__PURE__ */ jsx(
329
+ "button",
330
+ {
331
+ type: "button",
332
+ className: "docx-hyperlink-dialog-close",
333
+ style: CLOSE_BUTTON_STYLE,
334
+ onClick: onClose,
335
+ "aria-label": t("common.closeDialog"),
336
+ children: "\xD7"
337
+ }
338
+ )
339
+ ] }),
340
+ /* @__PURE__ */ jsxs(
341
+ "form",
342
+ {
343
+ className: "docx-hyperlink-dialog-body",
344
+ style: DIALOG_BODY_STYLE,
345
+ onSubmit: handleSubmit,
346
+ children: [
347
+ hasBookmarks && /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-tabs", style: TAB_CONTAINER_STYLE, children: [
348
+ /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ className: `docx-hyperlink-dialog-tab ${linkType === "url" ? "active" : ""}`,
353
+ style: linkType === "url" ? TAB_BUTTON_ACTIVE_STYLE : TAB_BUTTON_STYLE,
354
+ onClick: () => setLinkType("url"),
355
+ "aria-selected": linkType === "url",
356
+ children: t("dialogs.hyperlink.tabWebAddress")
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsx(
360
+ "button",
361
+ {
362
+ type: "button",
363
+ className: `docx-hyperlink-dialog-tab ${linkType === "bookmark" ? "active" : ""}`,
364
+ style: linkType === "bookmark" ? TAB_BUTTON_ACTIVE_STYLE : TAB_BUTTON_STYLE,
365
+ onClick: () => setLinkType("bookmark"),
366
+ "aria-selected": linkType === "bookmark",
367
+ children: t("dialogs.hyperlink.tabBookmark")
368
+ }
369
+ )
370
+ ] }),
371
+ linkType === "url" && /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-field", style: FORM_GROUP_STYLE, children: [
372
+ /* @__PURE__ */ jsx("label", { htmlFor: "hyperlink-url", style: LABEL_STYLE, children: t("dialogs.hyperlink.urlLabel") }),
373
+ /* @__PURE__ */ jsx(
374
+ "input",
375
+ {
376
+ ref: urlInputRef,
377
+ id: "hyperlink-url",
378
+ type: "text",
379
+ className: "docx-hyperlink-dialog-input",
380
+ style: urlError && touched ? INPUT_ERROR_STYLE : INPUT_STYLE,
381
+ value: url,
382
+ onChange: (e) => {
383
+ setUrl(e.target.value);
384
+ if (touched) setUrlError("");
385
+ },
386
+ onBlur: () => {
387
+ setTouched(true);
388
+ validateUrl();
389
+ },
390
+ placeholder: t("dialogs.hyperlink.urlPlaceholder"),
391
+ "aria-invalid": !!urlError,
392
+ "aria-describedby": urlError ? "url-error" : "url-hint"
393
+ }
394
+ ),
395
+ urlError && touched && /* @__PURE__ */ jsx("div", { id: "url-error", style: ERROR_TEXT_STYLE, children: urlError }),
396
+ !urlError && /* @__PURE__ */ jsx("div", { id: "url-hint", style: HINT_TEXT_STYLE, children: t("dialogs.hyperlink.urlHint") })
397
+ ] }),
398
+ linkType === "bookmark" && /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-field", style: FORM_GROUP_STYLE, children: [
399
+ /* @__PURE__ */ jsx("label", { htmlFor: "hyperlink-bookmark", style: LABEL_STYLE, children: t("dialogs.hyperlink.bookmarkLabel") }),
400
+ /* @__PURE__ */ jsxs(
401
+ "select",
402
+ {
403
+ ref: bookmarkSelectRef,
404
+ id: "hyperlink-bookmark",
405
+ className: "docx-hyperlink-dialog-select",
406
+ style: SELECT_STYLE,
407
+ value: bookmark,
408
+ onChange: (e) => setBookmark(e.target.value),
409
+ children: [
410
+ /* @__PURE__ */ jsx("option", { value: "", children: t("dialogs.hyperlink.bookmarkPlaceholder") }),
411
+ bookmarks.map((bm) => /* @__PURE__ */ jsx("option", { value: bm.name, children: bm.label || bm.name }, bm.name))
412
+ ]
413
+ }
414
+ )
415
+ ] }),
416
+ /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-field", style: FORM_GROUP_STYLE, children: [
417
+ /* @__PURE__ */ jsx("label", { htmlFor: "hyperlink-display-text", style: LABEL_STYLE, children: t("dialogs.hyperlink.displayTextLabel") }),
418
+ /* @__PURE__ */ jsx(
419
+ "input",
420
+ {
421
+ id: "hyperlink-display-text",
422
+ type: "text",
423
+ className: "docx-hyperlink-dialog-input",
424
+ style: INPUT_STYLE,
425
+ value: displayText,
426
+ onChange: (e) => setDisplayText(e.target.value),
427
+ placeholder: t("dialogs.hyperlink.displayTextPlaceholder")
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsx("div", { style: HINT_TEXT_STYLE, children: t("dialogs.hyperlink.displayTextHint") })
431
+ ] }),
432
+ /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-field", style: FORM_GROUP_STYLE, children: [
433
+ /* @__PURE__ */ jsx("label", { htmlFor: "hyperlink-tooltip", style: LABEL_STYLE, children: t("dialogs.hyperlink.tooltipLabel") }),
434
+ /* @__PURE__ */ jsx(
435
+ "input",
436
+ {
437
+ id: "hyperlink-tooltip",
438
+ type: "text",
439
+ className: "docx-hyperlink-dialog-input",
440
+ style: INPUT_STYLE,
441
+ value: tooltip,
442
+ onChange: (e) => setTooltip(e.target.value),
443
+ placeholder: t("dialogs.hyperlink.tooltipPlaceholder")
444
+ }
445
+ )
446
+ ] })
447
+ ]
448
+ }
449
+ ),
450
+ /* @__PURE__ */ jsxs("div", { className: "docx-hyperlink-dialog-footer", style: DIALOG_FOOTER_STYLE, children: [
451
+ isEditing && onRemove && /* @__PURE__ */ jsx(
452
+ "button",
453
+ {
454
+ type: "button",
455
+ className: "docx-hyperlink-dialog-remove",
456
+ style: DANGER_BUTTON_STYLE,
457
+ onClick: onRemove,
458
+ children: t("dialogs.hyperlink.removeLink")
459
+ }
460
+ ),
461
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
462
+ /* @__PURE__ */ jsx(
463
+ "button",
464
+ {
465
+ type: "button",
466
+ className: "docx-hyperlink-dialog-cancel",
467
+ style: SECONDARY_BUTTON_STYLE,
468
+ onClick: onClose,
469
+ children: t("common.cancel")
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsx(
473
+ "button",
474
+ {
475
+ type: "submit",
476
+ className: "docx-hyperlink-dialog-submit",
477
+ style: canSubmit ? PRIMARY_BUTTON_STYLE : DISABLED_BUTTON_STYLE,
478
+ onClick: handleSubmit,
479
+ disabled: !canSubmit,
480
+ children: isEditing ? t("common.update") : t("common.insert")
481
+ }
482
+ )
483
+ ] })
484
+ ] })
485
+ }
486
+ );
487
+ }
488
+ function createHyperlinkData(url, displayText) {
489
+ return {
490
+ url: normalizeUrl(url),
491
+ displayText
492
+ };
493
+ }
494
+ function createBookmarkLinkData(bookmark, displayText) {
495
+ return {
496
+ bookmark,
497
+ displayText
498
+ };
499
+ }
500
+ function isExternalHyperlinkData(data) {
501
+ return !!data.url && !data.bookmark;
502
+ }
503
+ function isBookmarkHyperlinkData(data) {
504
+ return !!data.bookmark;
505
+ }
506
+ function getDisplayText(data) {
507
+ if (data.displayText) {
508
+ return data.displayText;
509
+ }
510
+ if (data.url) {
511
+ return data.url.replace(/^https?:\/\//, "");
512
+ }
513
+ if (data.bookmark) {
514
+ return data.bookmark;
515
+ }
516
+ return "";
517
+ }
518
+ function emailToMailto(email) {
519
+ if (email.startsWith("mailto:")) {
520
+ return email;
521
+ }
522
+ return `mailto:${email}`;
523
+ }
524
+ function phoneToTel(phone) {
525
+ if (phone.startsWith("tel:")) {
526
+ return phone;
527
+ }
528
+ const cleaned = phone.replace(/[\s\-\(\)\.]/g, "");
529
+ return `tel:${cleaned}`;
530
+ }
531
+ function extractBookmarksForDialog(bookmarks) {
532
+ return bookmarks.filter((bm) => !bm.name.startsWith("_")).map((bm) => ({
533
+ name: bm.name,
534
+ label: bm.name
535
+ })).sort((a, b) => a.name.localeCompare(b.name));
536
+ }
537
+ function useHyperlinkDialog() {
538
+ const [state, setState] = useState({
539
+ isOpen: false,
540
+ isEditing: false
541
+ });
542
+ const openInsert = useCallback((selectedText) => {
543
+ setState({
544
+ isOpen: true,
545
+ selectedText,
546
+ initialData: void 0,
547
+ isEditing: false
548
+ });
549
+ }, []);
550
+ const openEdit = useCallback((data) => {
551
+ setState({
552
+ isOpen: true,
553
+ initialData: data,
554
+ selectedText: data.displayText,
555
+ isEditing: true
556
+ });
557
+ }, []);
558
+ const close = useCallback(() => {
559
+ setState((prev) => ({
560
+ ...prev,
561
+ isOpen: false
562
+ }));
563
+ }, []);
564
+ const toggle = useCallback(() => {
565
+ setState((prev) => ({
566
+ ...prev,
567
+ isOpen: !prev.isOpen
568
+ }));
569
+ }, []);
570
+ return { state, openInsert, openEdit, close, toggle };
571
+ }
572
+ var HyperlinkDialog_default = HyperlinkDialog;
573
+
574
+ export {
575
+ isValidUrl,
576
+ normalizeUrl,
577
+ getUrlType,
578
+ HyperlinkDialog,
579
+ createHyperlinkData,
580
+ createBookmarkLinkData,
581
+ isExternalHyperlinkData,
582
+ isBookmarkHyperlinkData,
583
+ getDisplayText,
584
+ emailToMailto,
585
+ phoneToTel,
586
+ extractBookmarksForDialog,
587
+ useHyperlinkDialog,
588
+ HyperlinkDialog_default
589
+ };