@crystallize/design-system 1.3.2 → 1.4.1
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.
- package/CHANGELOG.md +12 -0
- package/dist/TableComponent-I2YOOYOU.css +281 -0
- package/dist/TableComponent-QINOO453.mjs +1377 -0
- package/dist/arrow-clockwise-Z2G6UEGP.svg +1 -0
- package/dist/arrow-counterclockwise-2O5EYVJT.svg +1 -0
- package/dist/bg-color-HB2WDYGO.svg +1 -0
- package/dist/camera-CR7D2PNH.svg +1 -0
- package/dist/caret-right-fill-FFBNEXVX.svg +1 -0
- package/dist/chat-square-quote-CI6PUJHH.svg +1 -0
- package/dist/chevron-down-3FRWSIKS.svg +1 -0
- package/dist/chunk-VUXQZRSP.mjs +737 -0
- package/dist/clipboard-OSEFDF25.svg +1 -0
- package/dist/close-FH57ZMJF.svg +1 -0
- package/dist/code-SEVR6TIQ.svg +1 -0
- package/dist/copy-DMGDODUL.svg +1 -0
- package/dist/diagram-2-CEJUD2B4.svg +1 -0
- package/dist/download-JXUGIUCX.svg +1 -0
- package/dist/draggable-block-menu-KKHDNKJA.svg +1 -0
- package/dist/dropdown-more-BHZ5COKX.svg +1 -0
- package/dist/file-image-TIQPFJX4.svg +1 -0
- package/dist/filetype-gif-OG2BEYYK.svg +1 -0
- package/dist/font-color-J4GA3ZJO.svg +1 -0
- package/dist/font-family-ZU5N6TTE.svg +1 -0
- package/dist/gear-ICMT4NTP.svg +1 -0
- package/dist/horizontal-rule-N6RD2V7H.svg +1 -0
- package/dist/indent-MJ6JIMCK.svg +1 -0
- package/dist/index.css +2711 -315
- package/dist/index.d.ts +145 -40
- package/dist/index.js +10376 -1481
- package/dist/index.mjs +7609 -746
- package/dist/journal-code-XUT44HDV.svg +1 -0
- package/dist/justify-J7X5JEEX.svg +1 -0
- package/dist/link-W52N4JKZ.svg +1 -0
- package/dist/list-ol-2ZEUN4Z7.svg +1 -0
- package/dist/list-ul-DVKNUP47.svg +1 -0
- package/dist/lock-WCYOZOHW.svg +1 -0
- package/dist/lock-fill-JZSKOSHK.svg +1 -0
- package/dist/markdown-4BGQNLLT.svg +1 -0
- package/dist/mic-H5FNOMM7.svg +1 -0
- package/dist/outdent-2LUMUMIP.svg +1 -0
- package/dist/paint-bucket-VCISMZTH.svg +1 -0
- package/dist/palette-SWGFPRWZ.svg +1 -0
- package/dist/pencil-fill-STFSC26F.svg +1 -0
- package/dist/plug-HGGGEVS3.svg +1 -0
- package/dist/plug-fill-OTG3U4TN.svg +1 -0
- package/dist/plus-CQISIKEC.svg +1 -0
- package/dist/plus-slash-minus-N22JU4TI.svg +1 -0
- package/dist/prettier-WUJ7B5NV.svg +1 -0
- package/dist/prettier-error-DYJSLYDP.svg +1 -0
- package/dist/square-check-UTG6FU6D.svg +1 -0
- package/dist/success-YVXUMPEZ.svg +1 -0
- package/dist/table-BR6DI4ZQ.svg +1 -0
- package/dist/text-center-UQI6PAEF.svg +1 -0
- package/dist/text-left-KT2B6TR3.svg +1 -0
- package/dist/text-paragraph-MFTUIIQG.svg +1 -0
- package/dist/text-right-SKELPISG.svg +1 -0
- package/dist/trash-UOM6D7TD.svg +1 -0
- package/dist/type-bold-PY7COC3N.svg +1 -0
- package/dist/type-h1-6KJP7YOM.svg +1 -0
- package/dist/type-h2-VHI2USC3.svg +1 -0
- package/dist/type-h3-JIU77CHO.svg +1 -0
- package/dist/type-h4-P5EHKDAL.svg +1 -0
- package/dist/type-h5-CS2KYVRG.svg +1 -0
- package/dist/type-h6-J2O74LJZ.svg +1 -0
- package/dist/type-italic-3DSFOSG2.svg +1 -0
- package/dist/type-strikethrough-E2KKQFSX.svg +1 -0
- package/dist/type-subscript-BMPTRIBU.svg +1 -0
- package/dist/type-superscript-EDF6EPAA.svg +1 -0
- package/dist/type-underline-CBFA5VLF.svg +1 -0
- package/dist/upload-Q6KICGZW.svg +1 -0
- package/dist/user-EOI2NEFZ.svg +1 -0
- package/package.json +30 -6
- package/src/dialog/dialog.tsx +1 -0
- package/src/icon-button/icon-button.css +16 -14
- package/src/index.ts +4 -4
- package/src/input/input.css +1 -1
- package/src/input-with-label/input-with-label.css +1 -1
- package/src/rich-text-editor/appSettings.ts +28 -0
- package/src/rich-text-editor/context/SettingsContext.tsx +71 -0
- package/src/rich-text-editor/context/SharedAutocompleteContext.tsx +60 -0
- package/src/rich-text-editor/context/SharedHistoryContext.tsx +25 -0
- package/src/rich-text-editor/hooks/useReport.ts +64 -0
- package/src/rich-text-editor/images/cat-typing.gif +0 -0
- package/src/rich-text-editor/images/emoji/1F600.png +0 -0
- package/src/rich-text-editor/images/emoji/1F641.png +0 -0
- package/src/rich-text-editor/images/emoji/1F642.png +0 -0
- package/src/rich-text-editor/images/emoji/2764.png +0 -0
- package/src/rich-text-editor/images/emoji/LICENSE.md +5 -0
- package/src/rich-text-editor/images/icons/LICENSE.md +5 -0
- package/src/rich-text-editor/images/icons/arrow-clockwise.svg +1 -0
- package/src/rich-text-editor/images/icons/arrow-counterclockwise.svg +1 -0
- package/src/rich-text-editor/images/icons/bg-color.svg +1 -0
- package/src/rich-text-editor/images/icons/camera.svg +1 -0
- package/src/rich-text-editor/images/icons/card-checklist.svg +1 -0
- package/src/rich-text-editor/images/icons/caret-right-fill.svg +1 -0
- package/src/rich-text-editor/images/icons/chat-left-text.svg +1 -0
- package/src/rich-text-editor/images/icons/chat-right-dots.svg +1 -0
- package/src/rich-text-editor/images/icons/chat-right-text.svg +1 -0
- package/src/rich-text-editor/images/icons/chat-right.svg +1 -0
- package/src/rich-text-editor/images/icons/chat-square-quote.svg +1 -0
- package/src/rich-text-editor/images/icons/chevron-down.svg +1 -0
- package/src/rich-text-editor/images/icons/clipboard.svg +1 -0
- package/src/rich-text-editor/images/icons/close.svg +1 -0
- package/src/rich-text-editor/images/icons/code.svg +1 -0
- package/src/rich-text-editor/images/icons/comments.svg +1 -0
- package/src/rich-text-editor/images/icons/copy.svg +1 -0
- package/src/rich-text-editor/images/icons/diagram-2.svg +1 -0
- package/src/rich-text-editor/images/icons/download.svg +1 -0
- package/src/rich-text-editor/images/icons/draggable-block-menu.svg +1 -0
- package/src/rich-text-editor/images/icons/dropdown-more.svg +1 -0
- package/src/rich-text-editor/images/icons/figma.svg +1 -0
- package/src/rich-text-editor/images/icons/file-image.svg +1 -0
- package/src/rich-text-editor/images/icons/filetype-gif.svg +1 -0
- package/src/rich-text-editor/images/icons/font-color.svg +1 -0
- package/src/rich-text-editor/images/icons/font-family.svg +1 -0
- package/src/rich-text-editor/images/icons/gear.svg +1 -0
- package/src/rich-text-editor/images/icons/horizontal-rule.svg +1 -0
- package/src/rich-text-editor/images/icons/indent.svg +1 -0
- package/src/rich-text-editor/images/icons/journal-code.svg +1 -0
- package/src/rich-text-editor/images/icons/journal-text.svg +1 -0
- package/src/rich-text-editor/images/icons/justify.svg +1 -0
- package/src/rich-text-editor/images/icons/link.svg +1 -0
- package/src/rich-text-editor/images/icons/list-ol.svg +1 -0
- package/src/rich-text-editor/images/icons/list-ul.svg +1 -0
- package/src/rich-text-editor/images/icons/lock-fill.svg +1 -0
- package/src/rich-text-editor/images/icons/lock.svg +1 -0
- package/src/rich-text-editor/images/icons/markdown.svg +1 -0
- package/src/rich-text-editor/images/icons/mic.svg +1 -0
- package/src/rich-text-editor/images/icons/outdent.svg +1 -0
- package/src/rich-text-editor/images/icons/paint-bucket.svg +1 -0
- package/src/rich-text-editor/images/icons/palette.svg +1 -0
- package/src/rich-text-editor/images/icons/pencil-fill.svg +1 -0
- package/src/rich-text-editor/images/icons/plug-fill.svg +1 -0
- package/src/rich-text-editor/images/icons/plug.svg +1 -0
- package/src/rich-text-editor/images/icons/plus-slash-minus.svg +1 -0
- package/src/rich-text-editor/images/icons/plus.svg +1 -0
- package/src/rich-text-editor/images/icons/prettier-error.svg +1 -0
- package/src/rich-text-editor/images/icons/prettier.svg +1 -0
- package/src/rich-text-editor/images/icons/send.svg +1 -0
- package/src/rich-text-editor/images/icons/square-check.svg +1 -0
- package/src/rich-text-editor/images/icons/sticky.svg +1 -0
- package/src/rich-text-editor/images/icons/success.svg +1 -0
- package/src/rich-text-editor/images/icons/table.svg +1 -0
- package/src/rich-text-editor/images/icons/text-center.svg +1 -0
- package/src/rich-text-editor/images/icons/text-left.svg +1 -0
- package/src/rich-text-editor/images/icons/text-paragraph.svg +1 -0
- package/src/rich-text-editor/images/icons/text-right.svg +1 -0
- package/src/rich-text-editor/images/icons/trash.svg +1 -0
- package/src/rich-text-editor/images/icons/trash3.svg +1 -0
- package/src/rich-text-editor/images/icons/tweet.svg +1 -0
- package/src/rich-text-editor/images/icons/type-bold.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h1.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h2.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h3.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h4.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h5.svg +1 -0
- package/src/rich-text-editor/images/icons/type-h6.svg +1 -0
- package/src/rich-text-editor/images/icons/type-italic.svg +1 -0
- package/src/rich-text-editor/images/icons/type-strikethrough.svg +1 -0
- package/src/rich-text-editor/images/icons/type-subscript.svg +1 -0
- package/src/rich-text-editor/images/icons/type-superscript.svg +1 -0
- package/src/rich-text-editor/images/icons/type-underline.svg +1 -0
- package/src/rich-text-editor/images/icons/upload.svg +1 -0
- package/src/rich-text-editor/images/icons/user.svg +1 -0
- package/src/rich-text-editor/images/icons/youtube.svg +1 -0
- package/src/rich-text-editor/images/image/LICENSE.md +5 -0
- package/src/rich-text-editor/images/landscape.jpg +0 -0
- package/src/rich-text-editor/images/logo.svg +1 -0
- package/src/rich-text-editor/images/yellow-flower-small.jpg +0 -0
- package/src/rich-text-editor/images/yellow-flower.jpg +0 -0
- package/src/rich-text-editor/index.ts +1 -0
- package/src/rich-text-editor/model/crystallize-rich-text-types/code.ts +39 -0
- package/src/rich-text-editor/model/crystallize-rich-text-types/headings.ts +12 -0
- package/src/rich-text-editor/model/crystallize-rich-text-types/index.ts +69 -0
- package/src/rich-text-editor/model/crystallize-rich-text-types/link.ts +9 -0
- package/src/rich-text-editor/model/crystallize-rich-text-types/table.ts +16 -0
- package/src/rich-text-editor/model/crystallize-to-lexical.ts +186 -0
- package/src/rich-text-editor/model/lexical-to-crystallize.ts +232 -0
- package/src/rich-text-editor/nodes/AutocompleteNode.tsx +96 -0
- package/src/rich-text-editor/nodes/BaseNodes.ts +45 -0
- package/src/rich-text-editor/nodes/KeywordNode.ts +73 -0
- package/src/rich-text-editor/nodes/TableCellNodes.ts +31 -0
- package/src/rich-text-editor/nodes/TableComponent.tsx +1547 -0
- package/src/rich-text-editor/nodes/TableNode.tsx +398 -0
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +83 -0
- package/src/rich-text-editor/plugins/AutoLinkPlugin/index.tsx +47 -0
- package/src/rich-text-editor/plugins/AutocompletePlugin/index.tsx +2536 -0
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +60 -0
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.css +14 -0
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +140 -0
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.css +46 -0
- package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +155 -0
- package/src/rich-text-editor/plugins/CodeHighlightPlugin/index.ts +21 -0
- package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +320 -0
- package/src/rich-text-editor/plugins/DragDropPastePlugin/index.ts +40 -0
- package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.css +36 -0
- package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.tsx +368 -0
- package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.css +40 -0
- package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +305 -0
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.css +128 -0
- package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +351 -0
- package/src/rich-text-editor/plugins/LinkPlugin/index.tsx +16 -0
- package/src/rich-text-editor/plugins/ListMaxIndentLevelPlugin/index.ts +86 -0
- package/src/rich-text-editor/plugins/MarkdownShortcutPlugin/index.tsx +16 -0
- package/src/rich-text-editor/plugins/MarkdownTransformers/index.ts +195 -0
- package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +49 -0
- package/src/rich-text-editor/plugins/SpeechToTextPlugin/index.ts +113 -0
- package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +65 -0
- package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +481 -0
- package/src/rich-text-editor/plugins/TableCellResizer/index.css +12 -0
- package/src/rich-text-editor/plugins/TableCellResizer/index.tsx +386 -0
- package/src/rich-text-editor/plugins/TablePlugin.tsx +190 -0
- package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +726 -0
- package/src/rich-text-editor/plugins/TreeViewPlugin/index.tsx +25 -0
- package/src/rich-text-editor/plugins/TypingPerfPlugin/index.ts +117 -0
- package/src/rich-text-editor/rich-text-editor.css +1396 -0
- package/src/rich-text-editor/rich-text-editor.stories.tsx +385 -0
- package/src/rich-text-editor/rich-text-editor.tsx +228 -0
- package/src/rich-text-editor/tests/rich-text-editor-basic-rendering.test.tsx +47 -0
- package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +39 -0
- package/src/rich-text-editor/tests/rich-text-editor-model-basics.test.tsx +56 -0
- package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +195 -0
- package/src/rich-text-editor/tests/rich-text-editor-onchange.test.tsx +37 -0
- package/src/rich-text-editor/tests/rich-text-editor-quote.test.tsx +36 -0
- package/src/rich-text-editor/tests/rich-text-editor-text-formats.test.tsx +135 -0
- package/src/rich-text-editor/tests/rich-text-editor-typing.test.tsx +73 -0
- package/src/rich-text-editor/tests/utils.ts +23 -0
- package/src/rich-text-editor/themes/PlaygroundEditorTheme.css +433 -0
- package/src/rich-text-editor/themes/PlaygroundEditorTheme.ts +113 -0
- package/src/rich-text-editor/types.ts +5 -0
- package/src/rich-text-editor/ui/ContentEditable.css +13 -0
- package/src/rich-text-editor/ui/ContentEditable.tsx +15 -0
- package/src/rich-text-editor/ui/LinkPreview.css +57 -0
- package/src/rich-text-editor/ui/LinkPreview.tsx +169 -0
- package/src/rich-text-editor/utils/environment.ts +1 -0
- package/src/rich-text-editor/utils/getDOMRangeRect.ts +42 -0
- package/src/rich-text-editor/utils/getSelectedNode.ts +27 -0
- package/src/rich-text-editor/utils/guard.ts +10 -0
- package/src/rich-text-editor/utils/isMobileWidth.ts +7 -0
- package/src/rich-text-editor/utils/joinClasses.ts +13 -0
- package/src/rich-text-editor/utils/point.ts +55 -0
- package/src/rich-text-editor/utils/rect.ts +158 -0
- package/src/rich-text-editor/utils/setFloatingElemPosition.ts +46 -0
- package/src/rich-text-editor/utils/swipe.ts +127 -0
- package/src/rich-text-editor/utils/url.ts +33 -0
- package/src/Tokens.stories.tsx +0 -18
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import './LinkPreview.css';
|
|
10
|
+
import { CSSProperties, Suspense, useEffect, useState } from 'react';
|
|
11
|
+
import { $getSelection, $isTextNode } from 'lexical';
|
|
12
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
13
|
+
|
|
14
|
+
import { Button } from '../../button';
|
|
15
|
+
|
|
16
|
+
type MetaTagBaseShared = {
|
|
17
|
+
description?: string;
|
|
18
|
+
image?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type MetaTags = {
|
|
23
|
+
facebook: MetaTagBaseShared;
|
|
24
|
+
google: MetaTagBaseShared;
|
|
25
|
+
linkedin: MetaTagBaseShared;
|
|
26
|
+
pinterest: MetaTagBaseShared;
|
|
27
|
+
slack: MetaTagBaseShared;
|
|
28
|
+
twitter: MetaTagBaseShared;
|
|
29
|
+
metatags: MetaTagBaseShared;
|
|
30
|
+
favicon?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type MetaTagsRequestResponse =
|
|
34
|
+
| (MetaTags & {
|
|
35
|
+
success: boolean;
|
|
36
|
+
})
|
|
37
|
+
| null;
|
|
38
|
+
|
|
39
|
+
// Cached responses or running request promises
|
|
40
|
+
const PREVIEW_CACHE: Record<string, Promise<void | MetaTagsRequestResponse> | { preview: MetaTagsRequestResponse }> =
|
|
41
|
+
{};
|
|
42
|
+
|
|
43
|
+
const URL_MATCHER =
|
|
44
|
+
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
|
45
|
+
|
|
46
|
+
function useSuspenseRequest(url: string) {
|
|
47
|
+
let cached = PREVIEW_CACHE[url];
|
|
48
|
+
|
|
49
|
+
if (!url.match(URL_MATCHER)) {
|
|
50
|
+
return { preview: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!cached) {
|
|
54
|
+
cached = PREVIEW_CACHE[url] = fetch(`https://service-metatags.crystallize.workers.dev/?url=${encodeURI(url)}`)
|
|
55
|
+
.then(response => response.json() as Promise<MetaTagsRequestResponse>)
|
|
56
|
+
.then(preview => {
|
|
57
|
+
PREVIEW_CACHE[url] = { preview };
|
|
58
|
+
return preview;
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
PREVIEW_CACHE[url] = { preview: null };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cached instanceof Promise) {
|
|
66
|
+
throw cached;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function LinkPreviewContent({
|
|
73
|
+
url,
|
|
74
|
+
}: Readonly<{
|
|
75
|
+
url: string;
|
|
76
|
+
}>): JSX.Element | null {
|
|
77
|
+
const [textContent, setTextContent] = useState('');
|
|
78
|
+
const { preview } = useSuspenseRequest(url);
|
|
79
|
+
const [editor] = useLexicalComposerContext();
|
|
80
|
+
|
|
81
|
+
const hasPreview = preview !== null && preview.google?.title;
|
|
82
|
+
|
|
83
|
+
// Get the textContent from the link node (if the link is a single node)
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
editor.update(() => {
|
|
86
|
+
const sel = $getSelection();
|
|
87
|
+
const nodes = sel?.getNodes();
|
|
88
|
+
if (hasPreview && nodes?.length === 1) {
|
|
89
|
+
const [firstNode] = nodes;
|
|
90
|
+
if ($isTextNode(firstNode)) {
|
|
91
|
+
setTextContent(firstNode.getTextContent());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}, [editor, preview]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Replace the current
|
|
99
|
+
*/
|
|
100
|
+
function useTitleForText() {
|
|
101
|
+
editor.update(() => {
|
|
102
|
+
const sel = $getSelection();
|
|
103
|
+
const nodes = sel?.getNodes();
|
|
104
|
+
if (hasPreview && nodes?.length === 1) {
|
|
105
|
+
const [firstNode] = nodes;
|
|
106
|
+
if ($isTextNode(firstNode)) {
|
|
107
|
+
// @ts-expect-error
|
|
108
|
+
firstNode.setTextContent(preview.google.title);
|
|
109
|
+
// @ts-expect-error
|
|
110
|
+
setTextContent(preview.google.title);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!hasPreview) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="LinkPreview__container">
|
|
122
|
+
{preview.google.image && (
|
|
123
|
+
<div className="LinkPreview__imageWrapper bg-purple-50-900">
|
|
124
|
+
<img src={preview.google.image} alt={preview.google.title} className="LinkPreview__image" />
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
{preview.google.title && <div className="LinkPreview__title">{preview.google.title}</div>}
|
|
128
|
+
{preview.google.description && <div className="LinkPreview__description">{preview.google.description}</div>}
|
|
129
|
+
{textContent && textContent !== preview.google.title ? (
|
|
130
|
+
<Button className="mb-4 ml-5" onClick={useTitleForText}>
|
|
131
|
+
Replace link text with its title
|
|
132
|
+
</Button>
|
|
133
|
+
) : null}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Glimmer(props: { style: CSSProperties; index: number }): JSX.Element {
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
className="LinkPreview__glimmer"
|
|
142
|
+
{...props}
|
|
143
|
+
style={{
|
|
144
|
+
animationDelay: String((props.index || 0) * 300),
|
|
145
|
+
...(props.style || {}),
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default function LinkPreview({
|
|
152
|
+
url,
|
|
153
|
+
}: Readonly<{
|
|
154
|
+
url: string;
|
|
155
|
+
}>): JSX.Element {
|
|
156
|
+
return (
|
|
157
|
+
<Suspense
|
|
158
|
+
fallback={
|
|
159
|
+
<>
|
|
160
|
+
<Glimmer style={{ height: '80px' }} index={0} />
|
|
161
|
+
<Glimmer style={{ width: '60%' }} index={1} />
|
|
162
|
+
<Glimmer style={{ width: '80%' }} index={2} />
|
|
163
|
+
</>
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
<LinkPreviewContent url={url} />
|
|
167
|
+
</Suspense>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const IS_APPLE: boolean = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export function getDOMRangeRect(nativeSelection: Selection, rootElement: HTMLElement): DOMRect {
|
|
9
|
+
const domRange = nativeSelection.getRangeAt(0);
|
|
10
|
+
|
|
11
|
+
let rect;
|
|
12
|
+
|
|
13
|
+
if (nativeSelection.anchorNode === rootElement) {
|
|
14
|
+
let inner = rootElement;
|
|
15
|
+
while (inner.firstElementChild != null) {
|
|
16
|
+
inner = inner.firstElementChild as HTMLElement;
|
|
17
|
+
}
|
|
18
|
+
rect = inner.getBoundingClientRect();
|
|
19
|
+
} else {
|
|
20
|
+
/**
|
|
21
|
+
* Catch errors that might occur during testing with
|
|
22
|
+
* @testing-library
|
|
23
|
+
*/
|
|
24
|
+
if ('getBoundingClientRect' in domRange) {
|
|
25
|
+
rect = domRange.getBoundingClientRect();
|
|
26
|
+
} else {
|
|
27
|
+
// @ts-expect-error
|
|
28
|
+
return {
|
|
29
|
+
height: 0,
|
|
30
|
+
width: 0,
|
|
31
|
+
x: 0,
|
|
32
|
+
y: 0,
|
|
33
|
+
bottom: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
top: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return rect;
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import {$isAtNodeEnd} from '@lexical/selection';
|
|
9
|
+
import {ElementNode, RangeSelection, TextNode} from 'lexical';
|
|
10
|
+
|
|
11
|
+
export function getSelectedNode(
|
|
12
|
+
selection: RangeSelection,
|
|
13
|
+
): TextNode | ElementNode {
|
|
14
|
+
const anchor = selection.anchor;
|
|
15
|
+
const focus = selection.focus;
|
|
16
|
+
const anchorNode = selection.anchor.getNode();
|
|
17
|
+
const focusNode = selection.focus.getNode();
|
|
18
|
+
if (anchorNode === focusNode) {
|
|
19
|
+
return anchorNode;
|
|
20
|
+
}
|
|
21
|
+
const isBackward = selection.isBackward();
|
|
22
|
+
if (isBackward) {
|
|
23
|
+
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
24
|
+
} else {
|
|
25
|
+
return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export function isHTMLElement(x: unknown): x is HTMLElement {
|
|
9
|
+
return x instanceof HTMLElement;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default function joinClasses(
|
|
10
|
+
...args: Array<string | boolean | null | undefined>
|
|
11
|
+
) {
|
|
12
|
+
return args.filter(Boolean).join(' ');
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export class Point {
|
|
9
|
+
private readonly _x: number;
|
|
10
|
+
private readonly _y: number;
|
|
11
|
+
|
|
12
|
+
constructor(x: number, y: number) {
|
|
13
|
+
this._x = x;
|
|
14
|
+
this._y = y;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get x(): number {
|
|
18
|
+
return this._x;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get y(): number {
|
|
22
|
+
return this._y;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public equals({x, y}: Point): boolean {
|
|
26
|
+
return this.x === x && this.y === y;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public calcDeltaXTo({x}: Point): number {
|
|
30
|
+
return this.x - x;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public calcDeltaYTo({y}: Point): number {
|
|
34
|
+
return this.y - y;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public calcHorizontalDistanceTo(point: Point): number {
|
|
38
|
+
return Math.abs(this.calcDeltaXTo(point));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public calcVerticalDistance(point: Point): number {
|
|
42
|
+
return Math.abs(this.calcDeltaYTo(point));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public calcDistanceTo(point: Point): number {
|
|
46
|
+
return Math.sqrt(
|
|
47
|
+
Math.pow(this.calcDeltaXTo(point), 2) +
|
|
48
|
+
Math.pow(this.calcDeltaYTo(point), 2),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isPoint(x: unknown): x is Point {
|
|
54
|
+
return x instanceof Point;
|
|
55
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import {isPoint, Point} from './point';
|
|
9
|
+
|
|
10
|
+
type ContainsPointReturn = {
|
|
11
|
+
result: boolean;
|
|
12
|
+
reason: {
|
|
13
|
+
isOnTopSide: boolean;
|
|
14
|
+
isOnBottomSide: boolean;
|
|
15
|
+
isOnLeftSide: boolean;
|
|
16
|
+
isOnRightSide: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class Rect {
|
|
21
|
+
private readonly _left: number;
|
|
22
|
+
private readonly _top: number;
|
|
23
|
+
private readonly _right: number;
|
|
24
|
+
private readonly _bottom: number;
|
|
25
|
+
|
|
26
|
+
constructor(left: number, top: number, right: number, bottom: number) {
|
|
27
|
+
const [physicTop, physicBottom] =
|
|
28
|
+
top <= bottom ? [top, bottom] : [bottom, top];
|
|
29
|
+
|
|
30
|
+
const [physicLeft, physicRight] =
|
|
31
|
+
left <= right ? [left, right] : [right, left];
|
|
32
|
+
|
|
33
|
+
this._top = physicTop;
|
|
34
|
+
this._right = physicRight;
|
|
35
|
+
this._left = physicLeft;
|
|
36
|
+
this._bottom = physicBottom;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get top(): number {
|
|
40
|
+
return this._top;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get right(): number {
|
|
44
|
+
return this._right;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get bottom(): number {
|
|
48
|
+
return this._bottom;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get left(): number {
|
|
52
|
+
return this._left;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get width(): number {
|
|
56
|
+
return Math.abs(this._left - this._right);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get height(): number {
|
|
60
|
+
return Math.abs(this._bottom - this._top);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public equals({top, left, bottom, right}: Rect): boolean {
|
|
64
|
+
return (
|
|
65
|
+
top === this._top &&
|
|
66
|
+
bottom === this._bottom &&
|
|
67
|
+
left === this._left &&
|
|
68
|
+
right === this._right
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public contains({x, y}: Point): ContainsPointReturn;
|
|
73
|
+
public contains({top, left, bottom, right}: Rect): boolean;
|
|
74
|
+
public contains(target: Point | Rect): boolean | ContainsPointReturn {
|
|
75
|
+
if (isPoint(target)) {
|
|
76
|
+
const {x, y} = target;
|
|
77
|
+
|
|
78
|
+
const isOnTopSide = y < this._top;
|
|
79
|
+
const isOnBottomSide = y > this._bottom;
|
|
80
|
+
const isOnLeftSide = x < this._left;
|
|
81
|
+
const isOnRightSide = x > this._right;
|
|
82
|
+
|
|
83
|
+
const result =
|
|
84
|
+
!isOnTopSide && !isOnBottomSide && !isOnLeftSide && !isOnRightSide;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
reason: {
|
|
88
|
+
isOnBottomSide,
|
|
89
|
+
isOnLeftSide,
|
|
90
|
+
isOnRightSide,
|
|
91
|
+
isOnTopSide,
|
|
92
|
+
},
|
|
93
|
+
result,
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
const {top, left, bottom, right} = target;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
top >= this._top &&
|
|
100
|
+
top <= this._bottom &&
|
|
101
|
+
bottom >= this._top &&
|
|
102
|
+
bottom <= this._bottom &&
|
|
103
|
+
left >= this._left &&
|
|
104
|
+
left <= this._right &&
|
|
105
|
+
right >= this._left &&
|
|
106
|
+
right <= this._right
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public intersectsWith(rect: Rect): boolean {
|
|
112
|
+
const {left: x1, top: y1, width: w1, height: h1} = rect;
|
|
113
|
+
const {left: x2, top: y2, width: w2, height: h2} = this;
|
|
114
|
+
const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2;
|
|
115
|
+
const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2;
|
|
116
|
+
const minX = x1 <= x2 ? x1 : x2;
|
|
117
|
+
const minY = y1 <= y2 ? y1 : y2;
|
|
118
|
+
return maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public generateNewRect({
|
|
122
|
+
left = this.left,
|
|
123
|
+
top = this.top,
|
|
124
|
+
right = this.right,
|
|
125
|
+
bottom = this.bottom,
|
|
126
|
+
}): Rect {
|
|
127
|
+
return new Rect(left, top, right, bottom);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static fromLTRB(
|
|
131
|
+
left: number,
|
|
132
|
+
top: number,
|
|
133
|
+
right: number,
|
|
134
|
+
bottom: number,
|
|
135
|
+
): Rect {
|
|
136
|
+
return new Rect(left, top, right, bottom);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static fromLWTH(
|
|
140
|
+
left: number,
|
|
141
|
+
width: number,
|
|
142
|
+
top: number,
|
|
143
|
+
height: number,
|
|
144
|
+
): Rect {
|
|
145
|
+
return new Rect(left, top, left + width, top + height);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static fromPoints(startPoint: Point, endPoint: Point): Rect {
|
|
149
|
+
const {y: top, x: left} = startPoint;
|
|
150
|
+
const {y: bottom, x: right} = endPoint;
|
|
151
|
+
return Rect.fromLTRB(left, top, right, bottom);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static fromDOM(dom: HTMLElement): Rect {
|
|
155
|
+
const {top, width, left, height} = dom.getBoundingClientRect();
|
|
156
|
+
return Rect.fromLWTH(left, width, top, height);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
const VERTICAL_GAP = 10;
|
|
9
|
+
const HORIZONTAL_OFFSET = 5;
|
|
10
|
+
|
|
11
|
+
export function setFloatingElemPosition(
|
|
12
|
+
targetRect: ClientRect | null,
|
|
13
|
+
floatingElem: HTMLElement,
|
|
14
|
+
anchorElem: HTMLElement,
|
|
15
|
+
verticalGap: number = VERTICAL_GAP,
|
|
16
|
+
horizontalOffset: number = HORIZONTAL_OFFSET,
|
|
17
|
+
): void {
|
|
18
|
+
const scrollerElem = anchorElem.parentElement;
|
|
19
|
+
|
|
20
|
+
if (targetRect === null || !scrollerElem) {
|
|
21
|
+
floatingElem.style.opacity = '0';
|
|
22
|
+
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
27
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
28
|
+
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
|
29
|
+
|
|
30
|
+
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
|
31
|
+
let left = targetRect.left - horizontalOffset;
|
|
32
|
+
|
|
33
|
+
if (top < editorScrollerRect.top) {
|
|
34
|
+
top += floatingElemRect.height + targetRect.height + verticalGap * 2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
|
38
|
+
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
top -= anchorElementRect.top;
|
|
42
|
+
left -= anchorElementRect.left;
|
|
43
|
+
|
|
44
|
+
floatingElem.style.opacity = '1';
|
|
45
|
+
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type Force = [number, number];
|
|
10
|
+
type Listener = (force: Force, e: TouchEvent) => void;
|
|
11
|
+
type ElementValues = {
|
|
12
|
+
start: null | Force;
|
|
13
|
+
listeners: Set<Listener>;
|
|
14
|
+
handleTouchstart: (e: TouchEvent) => void;
|
|
15
|
+
handleTouchend: (e: TouchEvent) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const elements = new WeakMap<HTMLElement, ElementValues>();
|
|
19
|
+
|
|
20
|
+
function readTouch(e: TouchEvent): [number, number] | null {
|
|
21
|
+
const touch = e.changedTouches[0];
|
|
22
|
+
if (touch === undefined) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return [touch.clientX, touch.clientY];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function addListener(element: HTMLElement, cb: Listener): () => void {
|
|
29
|
+
let elementValues = elements.get(element);
|
|
30
|
+
if (elementValues === undefined) {
|
|
31
|
+
const listeners = new Set<Listener>();
|
|
32
|
+
const handleTouchstart = (e: TouchEvent) => {
|
|
33
|
+
if (elementValues !== undefined) {
|
|
34
|
+
elementValues.start = readTouch(e);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleTouchend = (e: TouchEvent) => {
|
|
38
|
+
if (elementValues === undefined) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const start = elementValues.start;
|
|
42
|
+
if (start === null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const end = readTouch(e);
|
|
46
|
+
for (const listener of listeners) {
|
|
47
|
+
if (end !== null) {
|
|
48
|
+
listener([end[0] - start[0], end[1] - start[1]], e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
element.addEventListener('touchstart', handleTouchstart);
|
|
53
|
+
element.addEventListener('touchend', handleTouchend);
|
|
54
|
+
|
|
55
|
+
elementValues = {
|
|
56
|
+
handleTouchend,
|
|
57
|
+
handleTouchstart,
|
|
58
|
+
listeners,
|
|
59
|
+
start: null,
|
|
60
|
+
};
|
|
61
|
+
elements.set(element, elementValues);
|
|
62
|
+
}
|
|
63
|
+
elementValues.listeners.add(cb);
|
|
64
|
+
return () => deleteListener(element, cb);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deleteListener(element: HTMLElement, cb: Listener): void {
|
|
68
|
+
const elementValues = elements.get(element);
|
|
69
|
+
if (elementValues === undefined) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const listeners = elementValues.listeners;
|
|
73
|
+
listeners.delete(cb);
|
|
74
|
+
if (listeners.size === 0) {
|
|
75
|
+
elements.delete(element);
|
|
76
|
+
element.removeEventListener('touchstart', elementValues.handleTouchstart);
|
|
77
|
+
element.removeEventListener('touchend', elementValues.handleTouchend);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function addSwipeLeftListener(
|
|
82
|
+
element: HTMLElement,
|
|
83
|
+
cb: (_force: number, e: TouchEvent) => void,
|
|
84
|
+
) {
|
|
85
|
+
return addListener(element, (force, e) => {
|
|
86
|
+
const [x, y] = force;
|
|
87
|
+
if (x < 0 && -x > Math.abs(y)) {
|
|
88
|
+
cb(x, e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function addSwipeRightListener(
|
|
94
|
+
element: HTMLElement,
|
|
95
|
+
cb: (_force: number, e: TouchEvent) => void,
|
|
96
|
+
) {
|
|
97
|
+
return addListener(element, (force, e) => {
|
|
98
|
+
const [x, y] = force;
|
|
99
|
+
if (x > 0 && x > Math.abs(y)) {
|
|
100
|
+
cb(x, e);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function addSwipeUpListener(
|
|
106
|
+
element: HTMLElement,
|
|
107
|
+
cb: (_force: number, e: TouchEvent) => void,
|
|
108
|
+
) {
|
|
109
|
+
return addListener(element, (force, e) => {
|
|
110
|
+
const [x, y] = force;
|
|
111
|
+
if (y < 0 && -y > Math.abs(x)) {
|
|
112
|
+
cb(x, e);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function addSwipeDownListener(
|
|
118
|
+
element: HTMLElement,
|
|
119
|
+
cb: (_force: number, e: TouchEvent) => void,
|
|
120
|
+
) {
|
|
121
|
+
return addListener(element, (force, e) => {
|
|
122
|
+
const [x, y] = force;
|
|
123
|
+
if (y > 0 && y > Math.abs(x)) {
|
|
124
|
+
cb(x, e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function sanitizeUrl(url: string): string {
|
|
10
|
+
/** A pattern that matches safe URLs. */
|
|
11
|
+
const SAFE_URL_PATTERN =
|
|
12
|
+
/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
|
|
13
|
+
|
|
14
|
+
/** A pattern that matches safe data URLs. */
|
|
15
|
+
const DATA_URL_PATTERN =
|
|
16
|
+
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
|
|
17
|
+
|
|
18
|
+
url = String(url).trim();
|
|
19
|
+
|
|
20
|
+
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
|
|
21
|
+
|
|
22
|
+
return 'https://';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Source: https://stackoverflow.com/a/8234912/2013580
|
|
26
|
+
const urlRegExp = new RegExp(
|
|
27
|
+
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/,
|
|
28
|
+
);
|
|
29
|
+
export function validateUrl(url: string): boolean {
|
|
30
|
+
// TODO Fix UI for link insertion; it should never default to an invalid URL such as https://.
|
|
31
|
+
// Maybe show a dialog where they user can type the URL before inserting it.
|
|
32
|
+
return url === 'https://' || urlRegExp.test(url);
|
|
33
|
+
}
|