@eventuras/scribo 0.4.2
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/LICENSE.md +31 -0
- package/README.md +31 -0
- package/eslint.config.js +4 -0
- package/index.html +13 -0
- package/package.json +41 -0
- package/src/App.css +35 -0
- package/src/App.tsx +42 -0
- package/src/MarkdownEditor.css +1127 -0
- package/src/MarkdownEditor.tsx +105 -0
- package/src/context/SharedHistoryContext.tsx +36 -0
- package/src/index.tsx +12 -0
- package/src/main.tsx +4 -0
- package/src/nodes/EditorNodes.ts +39 -0
- package/src/plugins/AutoLinkPlugin.tsx +34 -0
- package/src/plugins/FloatingLinkEditorPlugin.css +41 -0
- package/src/plugins/FloatingLinkEditorPlugin.tsx +469 -0
- package/src/plugins/HistoryPlugin.tsx +506 -0
- package/src/plugins/LinkPlugin.tsx +17 -0
- package/src/plugins/ToolbarPlugin.tsx +528 -0
- package/src/themes/EditorTheme.css +297 -0
- package/src/themes/EditorTheme.ts +118 -0
- package/src/ui/ContentEditable.css +23 -0
- package/src/ui/ContentEditable.tsx +19 -0
- package/src/ui/DropDown.tsx +262 -0
- package/src/ui/Placeholder.css +28 -0
- package/src/ui/Placeholder.tsx +21 -0
- package/src/utils/environment.ts +54 -0
- package/src/utils/getSelectedNode.ts +28 -0
- package/src/utils/setFloatingElemPosition.ts +51 -0
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
- package/src/utils/url.ts +39 -0
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
10
|
+
.Placeholder__root {
|
|
11
|
+
font-size: 15px;
|
|
12
|
+
color: #999;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
position: absolute;
|
|
15
|
+
text-overflow: ellipsis;
|
|
16
|
+
top: 8px;
|
|
17
|
+
left: 28px;
|
|
18
|
+
right: 28px;
|
|
19
|
+
user-select: none;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
display: inline-block;
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
}
|
|
24
|
+
@media (max-width: 1025px) {
|
|
25
|
+
.Placeholder__root {
|
|
26
|
+
left: 8px;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 type {JSX} from 'react';
|
|
9
|
+
import "./Placeholder.css";
|
|
10
|
+
|
|
11
|
+
import { ReactNode } from "react";
|
|
12
|
+
|
|
13
|
+
export default function Placeholder({
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
}: {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}): JSX.Element {
|
|
20
|
+
return <div className={className || "Placeholder__root"}>{children}</div>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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 const CAN_USE_DOM: boolean =
|
|
10
|
+
typeof window !== "undefined" &&
|
|
11
|
+
typeof window.document !== "undefined" &&
|
|
12
|
+
typeof window.document.createElement !== "undefined";
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface Document {
|
|
16
|
+
documentMode?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Window {
|
|
20
|
+
MSStream?: unknown;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const documentMode =
|
|
25
|
+
CAN_USE_DOM && "documentMode" in document ? document.documentMode : null;
|
|
26
|
+
|
|
27
|
+
//TODO forcing false on this to stop mismatch render between ssr and clientside
|
|
28
|
+
export const IS_APPLE = false;
|
|
29
|
+
//CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
30
|
+
|
|
31
|
+
export const IS_FIREFOX: boolean =
|
|
32
|
+
CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
|
|
33
|
+
|
|
34
|
+
export const CAN_USE_BEFORE_INPUT: boolean =
|
|
35
|
+
CAN_USE_DOM && "InputEvent" in window && !documentMode
|
|
36
|
+
? "getTargetRanges" in new window.InputEvent("input")
|
|
37
|
+
: false;
|
|
38
|
+
|
|
39
|
+
export const IS_SAFARI: boolean =
|
|
40
|
+
CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
|
|
41
|
+
|
|
42
|
+
export const IS_IOS: boolean =
|
|
43
|
+
CAN_USE_DOM &&
|
|
44
|
+
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
|
45
|
+
!window.MSStream;
|
|
46
|
+
|
|
47
|
+
// Keep these in case we need to use them in the future.
|
|
48
|
+
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
|
49
|
+
export const IS_CHROME: boolean =
|
|
50
|
+
CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
|
|
51
|
+
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
|
|
52
|
+
|
|
53
|
+
export const IS_APPLE_WEBKIT =
|
|
54
|
+
CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME;
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* source: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/utils/getSelectedNode.ts
|
|
8
|
+
*/
|
|
9
|
+
import { $isAtNodeEnd } from "@lexical/selection";
|
|
10
|
+
import { ElementNode, RangeSelection, TextNode } from "lexical";
|
|
11
|
+
|
|
12
|
+
export function getSelectedNode(
|
|
13
|
+
selection: RangeSelection,
|
|
14
|
+
): TextNode | ElementNode {
|
|
15
|
+
const anchor = selection.anchor;
|
|
16
|
+
const focus = selection.focus;
|
|
17
|
+
const anchorNode = selection.anchor.getNode();
|
|
18
|
+
const focusNode = selection.focus.getNode();
|
|
19
|
+
if (anchorNode === focusNode) {
|
|
20
|
+
return anchorNode;
|
|
21
|
+
}
|
|
22
|
+
const isBackward = selection.isBackward();
|
|
23
|
+
if (isBackward) {
|
|
24
|
+
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
25
|
+
} else {
|
|
26
|
+
return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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: DOMRect | null,
|
|
13
|
+
floatingElem: HTMLElement,
|
|
14
|
+
anchorElem: HTMLElement,
|
|
15
|
+
isLink: boolean = false,
|
|
16
|
+
verticalGap: number = VERTICAL_GAP,
|
|
17
|
+
horizontalOffset: number = HORIZONTAL_OFFSET,
|
|
18
|
+
): void {
|
|
19
|
+
const scrollerElem = anchorElem.parentElement;
|
|
20
|
+
|
|
21
|
+
if (targetRect === null || !scrollerElem) {
|
|
22
|
+
floatingElem.style.opacity = '0';
|
|
23
|
+
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
28
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
29
|
+
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
|
30
|
+
|
|
31
|
+
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
|
32
|
+
let left = targetRect.left - horizontalOffset;
|
|
33
|
+
|
|
34
|
+
if (top < editorScrollerRect.top) {
|
|
35
|
+
// adjusted height for link element if the element is at top
|
|
36
|
+
top +=
|
|
37
|
+
floatingElemRect.height +
|
|
38
|
+
targetRect.height +
|
|
39
|
+
verticalGap * (isLink ? 9 : 2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
|
43
|
+
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
top -= anchorElementRect.top;
|
|
47
|
+
left -= anchorElementRect.left;
|
|
48
|
+
|
|
49
|
+
floatingElem.style.opacity = '1';
|
|
50
|
+
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
51
|
+
}
|
|
@@ -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 setFloatingElemPositionForLinkEditor(
|
|
12
|
+
targetRect: DOMRect | 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 - 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
|
+
}
|
package/src/utils/url.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
* Source: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/utils/url.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_URL_PROTOCOLS = new Set([
|
|
11
|
+
"http:",
|
|
12
|
+
"https:",
|
|
13
|
+
"mailto:",
|
|
14
|
+
"sms:",
|
|
15
|
+
"tel:",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function sanitizeUrl(url: string): string {
|
|
19
|
+
try {
|
|
20
|
+
const parsedUrl = new URL(url);
|
|
21
|
+
// eslint-disable-next-line no-script-url
|
|
22
|
+
if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
|
|
23
|
+
return "about:blank";
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Source: https://stackoverflow.com/a/8234912/2013580
|
|
32
|
+
const urlRegExp = new RegExp(
|
|
33
|
+
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/,
|
|
34
|
+
);
|
|
35
|
+
export function validateUrl(url: string): boolean {
|
|
36
|
+
// TODO Fix UI for link insertion; it should never default to an invalid URL such as https://.
|
|
37
|
+
// Maybe show a dialog where they user can type the URL before inserting it.
|
|
38
|
+
return url === "https://" || urlRegExp.test(url);
|
|
39
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react-swc';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
// Check build mode
|
|
6
|
+
const buildSite = process.env.BUILD_SITE === 'true';
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [react()],
|
|
10
|
+
build: buildSite
|
|
11
|
+
? {
|
|
12
|
+
// Build demo site
|
|
13
|
+
outDir: 'dist/site',
|
|
14
|
+
}
|
|
15
|
+
: {
|
|
16
|
+
// Build library
|
|
17
|
+
lib: {
|
|
18
|
+
entry: resolve(__dirname, 'src/main.tsx'),
|
|
19
|
+
name: 'Scribo',
|
|
20
|
+
fileName: 'scribo',
|
|
21
|
+
},
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
external: ['react', 'react-dom'],
|
|
24
|
+
output: {
|
|
25
|
+
globals: {
|
|
26
|
+
react: 'React',
|
|
27
|
+
'react-dom': 'ReactDOM',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|