@lalternative/editor 0.1.0 → 0.2.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.
- package/dist/diff-uFiP8hD5.d.ts +52 -0
- package/dist/editor.css +93 -0
- package/dist/index.d.ts +79 -39
- package/dist/index.js +209 -1
- package/dist/index.js.map +1 -1
- package/dist/revisions.d.ts +2 -2
- package/package.json +6 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RichNode is one node of a TipTap document.
|
|
3
|
+
*
|
|
4
|
+
* The index signature is what lets a document cross the boundary with an
|
|
5
|
+
* editor handle typed as a plain object, which is how TipTap hands its JSON
|
|
6
|
+
* back. Without it every call site pays for the mismatch in casts.
|
|
7
|
+
*/
|
|
8
|
+
interface RichNode {
|
|
9
|
+
type?: string;
|
|
10
|
+
attrs?: Record<string, unknown>;
|
|
11
|
+
content?: RichNode[];
|
|
12
|
+
text?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
type RichDoc = RichNode;
|
|
16
|
+
interface BlockShape {
|
|
17
|
+
type: "paragraph" | "heading";
|
|
18
|
+
level?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* blocksFromDoc flattens a TipTap document to the text of its top-level nodes.
|
|
22
|
+
*
|
|
23
|
+
* Marks are dropped rather than encoded: the diff compares prose, and a bold
|
|
24
|
+
* span that moved inside an otherwise identical sentence is not a change worth
|
|
25
|
+
* asking the reviewer about.
|
|
26
|
+
*/
|
|
27
|
+
declare function blocksFromDoc(doc: RichDoc): string[];
|
|
28
|
+
/** shapesFromDoc records what each block was, so a revision can be rebuilt
|
|
29
|
+
* without turning every heading into a paragraph. */
|
|
30
|
+
declare function shapesFromDoc(doc: RichDoc): BlockShape[];
|
|
31
|
+
declare function docFromBlocks(blocks: string[], shapes?: BlockShape[]): RichDoc;
|
|
32
|
+
|
|
33
|
+
type ChangeKind = "keep" | "insert" | "delete" | "replace";
|
|
34
|
+
interface BlockChange {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: ChangeKind;
|
|
37
|
+
before: string;
|
|
38
|
+
after: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* diffBlocks compares two documents one block at a time.
|
|
42
|
+
*
|
|
43
|
+
* The block is the unit because it is the unit a reviewer accepts or rejects:
|
|
44
|
+
* a finer diff turns one editorial decision into a dozen clicks, and a coarser
|
|
45
|
+
* one makes rejecting a single sentence cost the whole section.
|
|
46
|
+
*
|
|
47
|
+
* Accepting every change rebuilds `after` exactly; rejecting every change
|
|
48
|
+
* rebuilds `before` exactly. The UI relies on both.
|
|
49
|
+
*/
|
|
50
|
+
declare function diffBlocks(before: string[], after: string[]): BlockChange[];
|
|
51
|
+
|
|
52
|
+
export { type BlockChange as B, type ChangeKind as C, type RichDoc as R, type BlockShape as a, type RichNode as b, blocksFromDoc as c, diffBlocks as d, docFromBlocks as e, shapesFromDoc as s };
|
package/dist/editor.css
CHANGED
|
@@ -5,6 +5,99 @@
|
|
|
5
5
|
--lalt-editor-insert-bg: rgb(29 78 216 / 0.08);
|
|
6
6
|
--lalt-editor-delete: #b91c1c;
|
|
7
7
|
--lalt-editor-delete-bg: rgb(185 28 28 / 0.06);
|
|
8
|
+
--lalt-editor-surface: #ffffff;
|
|
9
|
+
--lalt-editor-border: rgb(0 0 0 / 0.1);
|
|
10
|
+
--lalt-editor-hover: rgb(0 0 0 / 0.06);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Defaults only: a host that owns a theme overrides these on :root and never
|
|
14
|
+
reaches this block. Without it the toolbar is a white slab on a dark page. */
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--lalt-editor-insert: #93c5fd;
|
|
18
|
+
--lalt-editor-insert-bg: rgb(147 197 253 / 0.12);
|
|
19
|
+
--lalt-editor-delete: #fca5a5;
|
|
20
|
+
--lalt-editor-delete-bg: rgb(252 165 165 / 0.1);
|
|
21
|
+
--lalt-editor-surface: #1f2937;
|
|
22
|
+
--lalt-editor-border: rgb(255 255 255 / 0.14);
|
|
23
|
+
--lalt-editor-hover: rgb(255 255 255 / 0.08);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.lalt-toolbar {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.125rem;
|
|
31
|
+
padding: 0.25rem;
|
|
32
|
+
border-radius: 0.5rem;
|
|
33
|
+
background: var(--lalt-editor-surface, #ffffff);
|
|
34
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
35
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
|
|
36
|
+
user-select: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.lalt-toolbar__button {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.375rem;
|
|
43
|
+
padding: 0.25rem 0.5rem;
|
|
44
|
+
border-radius: 0.375rem;
|
|
45
|
+
background: none;
|
|
46
|
+
border: none;
|
|
47
|
+
font: inherit;
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
color: inherit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.lalt-toolbar__button:hover {
|
|
54
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.lalt-toolbar__formats {
|
|
58
|
+
position: relative;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.lalt-toolbar__menu {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: calc(100% + 0.375rem);
|
|
64
|
+
right: 0;
|
|
65
|
+
min-width: 14rem;
|
|
66
|
+
padding: 0.25rem;
|
|
67
|
+
border-radius: 0.5rem;
|
|
68
|
+
background: var(--lalt-editor-surface, #ffffff);
|
|
69
|
+
border: 1px solid var(--lalt-editor-border, rgb(0 0 0 / 0.1));
|
|
70
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.lalt-toolbar__item {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
gap: 1.5rem;
|
|
78
|
+
width: 100%;
|
|
79
|
+
padding: 0.375rem 0.5rem;
|
|
80
|
+
border-radius: 0.375rem;
|
|
81
|
+
background: none;
|
|
82
|
+
border: none;
|
|
83
|
+
font: inherit;
|
|
84
|
+
font-size: 0.875rem;
|
|
85
|
+
text-align: left;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
color: inherit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lalt-toolbar__item:hover {
|
|
91
|
+
background: var(--lalt-editor-hover, rgb(0 0 0 / 0.06));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.lalt-toolbar__item[aria-current="true"] {
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.lalt-toolbar__shortcut {
|
|
99
|
+
font-size: 0.75rem;
|
|
100
|
+
opacity: 0.5;
|
|
8
101
|
}
|
|
9
102
|
|
|
10
103
|
.lalt-revision {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,52 +1,92 @@
|
|
|
1
|
+
export { B as BlockChange, a as BlockShape, C as ChangeKind, R as RichDoc, b as RichNode, c as blocksFromDoc, d as diffBlocks, e as docFromBlocks, s as shapesFromDoc } from './diff-uFiP8hD5.js';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { Editor } from '@tiptap/react';
|
|
4
|
+
|
|
5
|
+
interface SelectionRect {
|
|
6
|
+
/** Horizontal centre of the selection. */
|
|
7
|
+
x: number;
|
|
8
|
+
top: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
}
|
|
11
|
+
interface Size {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
interface PlaceToolbarInput {
|
|
16
|
+
selection: SelectionRect;
|
|
17
|
+
toolbar: Size;
|
|
18
|
+
viewport: Size;
|
|
19
|
+
/** iOS renders its own callout above a selection, which covers a toolbar
|
|
20
|
+
* placed there, so the caller asks for below on that platform. */
|
|
21
|
+
preferBelow?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface ToolbarPlacement {
|
|
24
|
+
left: number;
|
|
25
|
+
top: number;
|
|
26
|
+
below: boolean;
|
|
27
|
+
}
|
|
1
28
|
/**
|
|
2
|
-
*
|
|
29
|
+
* placeToolbar positions a floating toolbar against a selection.
|
|
3
30
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
31
|
+
* Kept apart from the component because it is the part that is wrong in subtle
|
|
32
|
+
* ways — off-screen at the edges, hidden under a system callout — and the part
|
|
33
|
+
* a test can pin down without a browser.
|
|
7
34
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
interface
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
declare function placeToolbar({ selection, toolbar, viewport, preferBelow, }: PlaceToolbarInput): ToolbarPlacement;
|
|
36
|
+
|
|
37
|
+
interface ToolbarAction {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
icon?: React.ReactNode;
|
|
41
|
+
onSelect: (selectedText: string) => void;
|
|
42
|
+
}
|
|
43
|
+
interface BlockFormat {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
shortcut?: string;
|
|
47
|
+
isActive?: boolean;
|
|
48
|
+
onSelect: () => void;
|
|
49
|
+
}
|
|
50
|
+
interface SelectionToolbarProps {
|
|
51
|
+
selectedText: string;
|
|
52
|
+
selection: SelectionRect;
|
|
53
|
+
actions?: ToolbarAction[];
|
|
54
|
+
/** Heading levels and list kinds, rendered behind a single trigger. */
|
|
55
|
+
formats?: BlockFormat[];
|
|
56
|
+
formatsLabel?: string;
|
|
57
|
+
/** iOS draws its own callout above a selection; the host detects it. */
|
|
58
|
+
preferBelow?: boolean;
|
|
59
|
+
container?: HTMLElement | null;
|
|
60
|
+
}
|
|
61
|
+
declare function SelectionToolbar({ selectedText, selection, actions, formats, formatsLabel, preferBelow, container, }: SelectionToolbarProps): react.ReactPortal | null;
|
|
62
|
+
|
|
63
|
+
interface SelectionInfo {
|
|
64
|
+
text: string;
|
|
65
|
+
rect: SelectionRect;
|
|
19
66
|
}
|
|
20
67
|
/**
|
|
21
|
-
*
|
|
68
|
+
* useSelectionToolbar reports the current non-empty selection of an editor.
|
|
22
69
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* asking the reviewer about.
|
|
70
|
+
* Returns null whenever there is nothing to act on, so the caller renders the
|
|
71
|
+
* toolbar only while it has a target.
|
|
26
72
|
*/
|
|
27
|
-
declare function
|
|
28
|
-
/** shapesFromDoc records what each block was, so a revision can be rebuilt
|
|
29
|
-
* without turning every heading into a paragraph. */
|
|
30
|
-
declare function shapesFromDoc(doc: RichDoc): BlockShape[];
|
|
31
|
-
declare function docFromBlocks(blocks: string[], shapes?: BlockShape[]): RichDoc;
|
|
73
|
+
declare function useSelectionToolbar(editor: Editor | null): SelectionInfo | null;
|
|
32
74
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
interface FormatLabels {
|
|
76
|
+
paragraph: string;
|
|
77
|
+
heading1: string;
|
|
78
|
+
heading2: string;
|
|
79
|
+
heading3: string;
|
|
80
|
+
orderedList: string;
|
|
81
|
+
bulletList: string;
|
|
39
82
|
}
|
|
83
|
+
declare const defaultFormatLabels: FormatLabels;
|
|
40
84
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* The block is the unit because it is the unit a reviewer accepts or rejects:
|
|
44
|
-
* a finer diff turns one editorial decision into a dozen clicks, and a coarser
|
|
45
|
-
* one makes rejecting a single sentence cost the whole section.
|
|
85
|
+
* blockFormats is the set a writer reaches for on a selected passage.
|
|
46
86
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
87
|
+
* Labels are passed in rather than translated here: the package carries no
|
|
88
|
+
* i18n runtime, so the host names things in whatever language it speaks.
|
|
49
89
|
*/
|
|
50
|
-
declare function
|
|
90
|
+
declare function blockFormats(editor: Editor | null, labels?: FormatLabels): BlockFormat[];
|
|
51
91
|
|
|
52
|
-
export { type
|
|
92
|
+
export { type BlockFormat, type FormatLabels, type PlaceToolbarInput, type SelectionInfo, type SelectionRect, SelectionToolbar, type SelectionToolbarProps, type ToolbarAction, type ToolbarPlacement, blockFormats, defaultFormatLabels, placeToolbar, useSelectionToolbar };
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,218 @@ import {
|
|
|
4
4
|
docFromBlocks,
|
|
5
5
|
shapesFromDoc
|
|
6
6
|
} from "./chunk-7GOSVZTI.js";
|
|
7
|
+
|
|
8
|
+
// src/placement.ts
|
|
9
|
+
var GAP = 8;
|
|
10
|
+
function placeToolbar({
|
|
11
|
+
selection,
|
|
12
|
+
toolbar,
|
|
13
|
+
viewport,
|
|
14
|
+
preferBelow = false
|
|
15
|
+
}) {
|
|
16
|
+
const fitsAbove = selection.top - GAP - toolbar.height >= 0;
|
|
17
|
+
const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;
|
|
18
|
+
const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
|
|
19
|
+
const top = below ? selection.bottom + GAP : Math.max(0, selection.top - GAP - toolbar.height);
|
|
20
|
+
const centred = selection.x - toolbar.width / 2;
|
|
21
|
+
const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));
|
|
22
|
+
return { left, top, below };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/SelectionToolbar.tsx
|
|
26
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
27
|
+
import { createPortal } from "react-dom";
|
|
28
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
+
var FALLBACK = { width: 320, height: 40 };
|
|
30
|
+
function SelectionToolbar({
|
|
31
|
+
selectedText,
|
|
32
|
+
selection,
|
|
33
|
+
actions = [],
|
|
34
|
+
formats = [],
|
|
35
|
+
formatsLabel = "Format",
|
|
36
|
+
preferBelow = false,
|
|
37
|
+
container
|
|
38
|
+
}) {
|
|
39
|
+
const ref = useRef(null);
|
|
40
|
+
const [size, setSize] = useState(FALLBACK);
|
|
41
|
+
const [formatsOpen, setFormatsOpen] = useState(false);
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
const el = ref.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
const rect = el.getBoundingClientRect();
|
|
46
|
+
if (rect.width > 0) setSize({ width: rect.width, height: rect.height });
|
|
47
|
+
}, [selectedText, actions.length, formats.length]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!formatsOpen) return;
|
|
50
|
+
const close = (e) => {
|
|
51
|
+
if (ref.current && !ref.current.contains(e.target)) setFormatsOpen(false);
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("mousedown", close);
|
|
54
|
+
return () => document.removeEventListener("mousedown", close);
|
|
55
|
+
}, [formatsOpen]);
|
|
56
|
+
if (typeof document === "undefined") return null;
|
|
57
|
+
const place = placeToolbar({
|
|
58
|
+
selection,
|
|
59
|
+
toolbar: size,
|
|
60
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
61
|
+
preferBelow
|
|
62
|
+
});
|
|
63
|
+
return createPortal(
|
|
64
|
+
/* @__PURE__ */ jsxs(
|
|
65
|
+
"div",
|
|
66
|
+
{
|
|
67
|
+
ref,
|
|
68
|
+
role: "toolbar",
|
|
69
|
+
className: "lalt-toolbar",
|
|
70
|
+
style: { position: "fixed", left: place.left, top: place.top, zIndex: 100 },
|
|
71
|
+
children: [
|
|
72
|
+
actions.map((action) => /* @__PURE__ */ jsxs(
|
|
73
|
+
"button",
|
|
74
|
+
{
|
|
75
|
+
type: "button",
|
|
76
|
+
className: "lalt-toolbar__button",
|
|
77
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
78
|
+
onClick: () => action.onSelect(selectedText),
|
|
79
|
+
children: [
|
|
80
|
+
action.icon,
|
|
81
|
+
/* @__PURE__ */ jsx("span", { children: action.label })
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
action.id
|
|
85
|
+
)),
|
|
86
|
+
formats.length > 0 && /* @__PURE__ */ jsxs("div", { className: "lalt-toolbar__formats", children: [
|
|
87
|
+
/* @__PURE__ */ jsx(
|
|
88
|
+
"button",
|
|
89
|
+
{
|
|
90
|
+
type: "button",
|
|
91
|
+
className: "lalt-toolbar__button",
|
|
92
|
+
"aria-haspopup": "menu",
|
|
93
|
+
"aria-expanded": formatsOpen,
|
|
94
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
95
|
+
onClick: () => setFormatsOpen((open) => !open),
|
|
96
|
+
children: formatsLabel
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
formatsOpen && /* @__PURE__ */ jsx("div", { role: "menu", className: "lalt-toolbar__menu", children: formats.map((format) => /* @__PURE__ */ jsxs(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
role: "menuitem",
|
|
104
|
+
className: "lalt-toolbar__item",
|
|
105
|
+
"aria-current": format.isActive,
|
|
106
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
107
|
+
onClick: () => {
|
|
108
|
+
format.onSelect();
|
|
109
|
+
setFormatsOpen(false);
|
|
110
|
+
},
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ jsx("span", { children: format.label }),
|
|
113
|
+
format.shortcut && /* @__PURE__ */ jsx("kbd", { className: "lalt-toolbar__shortcut", children: format.shortcut })
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
format.id
|
|
117
|
+
)) })
|
|
118
|
+
] })
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
container ?? document.body
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/useSelectionToolbar.ts
|
|
127
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
128
|
+
function useSelectionToolbar(editor) {
|
|
129
|
+
const [selection, setSelection] = useState2(null);
|
|
130
|
+
useEffect2(() => {
|
|
131
|
+
if (!editor) return;
|
|
132
|
+
const read = () => {
|
|
133
|
+
const { state } = editor;
|
|
134
|
+
const { from, to, empty } = state.selection;
|
|
135
|
+
if (empty || !editor.isFocused) {
|
|
136
|
+
setSelection(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const text = state.doc.textBetween(from, to, " ").trim();
|
|
140
|
+
if (text === "") {
|
|
141
|
+
setSelection(null);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const start = editor.view.coordsAtPos(from);
|
|
145
|
+
const end = editor.view.coordsAtPos(to);
|
|
146
|
+
setSelection({
|
|
147
|
+
text,
|
|
148
|
+
rect: {
|
|
149
|
+
x: (start.left + end.right) / 2,
|
|
150
|
+
top: Math.min(start.top, end.top),
|
|
151
|
+
bottom: Math.max(start.bottom, end.bottom)
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
read();
|
|
156
|
+
editor.on("selectionUpdate", read);
|
|
157
|
+
editor.on("blur", read);
|
|
158
|
+
editor.on("focus", read);
|
|
159
|
+
return () => {
|
|
160
|
+
editor.off("selectionUpdate", read);
|
|
161
|
+
editor.off("blur", read);
|
|
162
|
+
editor.off("focus", read);
|
|
163
|
+
};
|
|
164
|
+
}, [editor]);
|
|
165
|
+
return selection;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/formats.ts
|
|
169
|
+
var defaultFormatLabels = {
|
|
170
|
+
paragraph: "Texte",
|
|
171
|
+
heading1: "Titre 1",
|
|
172
|
+
heading2: "Titre 2",
|
|
173
|
+
heading3: "Titre 3",
|
|
174
|
+
orderedList: "Liste num\xE9rot\xE9e",
|
|
175
|
+
bulletList: "Liste \xE0 puces"
|
|
176
|
+
};
|
|
177
|
+
function blockFormats(editor, labels = defaultFormatLabels) {
|
|
178
|
+
if (!editor) return [];
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
id: "paragraph",
|
|
182
|
+
label: labels.paragraph,
|
|
183
|
+
shortcut: "Ctrl+Alt+0",
|
|
184
|
+
isActive: editor.isActive("paragraph"),
|
|
185
|
+
onSelect: () => editor.chain().focus().setParagraph().run()
|
|
186
|
+
},
|
|
187
|
+
...[1, 2, 3].map((level) => ({
|
|
188
|
+
id: `heading${level}`,
|
|
189
|
+
label: labels[`heading${level}`],
|
|
190
|
+
shortcut: `Ctrl+Alt+${level}`,
|
|
191
|
+
isActive: editor.isActive("heading", { level }),
|
|
192
|
+
onSelect: () => editor.chain().focus().toggleHeading({ level }).run()
|
|
193
|
+
})),
|
|
194
|
+
{
|
|
195
|
+
id: "orderedList",
|
|
196
|
+
label: labels.orderedList,
|
|
197
|
+
shortcut: "Ctrl+Alt+4",
|
|
198
|
+
isActive: editor.isActive("orderedList"),
|
|
199
|
+
onSelect: () => editor.chain().focus().toggleOrderedList().run()
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "bulletList",
|
|
203
|
+
label: labels.bulletList,
|
|
204
|
+
shortcut: "Ctrl+Alt+5",
|
|
205
|
+
isActive: editor.isActive("bulletList"),
|
|
206
|
+
onSelect: () => editor.chain().focus().toggleBulletList().run()
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
}
|
|
7
210
|
export {
|
|
211
|
+
SelectionToolbar,
|
|
212
|
+
blockFormats,
|
|
8
213
|
blocksFromDoc,
|
|
214
|
+
defaultFormatLabels,
|
|
9
215
|
diffBlocks,
|
|
10
216
|
docFromBlocks,
|
|
11
|
-
|
|
217
|
+
placeToolbar,
|
|
218
|
+
shapesFromDoc,
|
|
219
|
+
useSelectionToolbar
|
|
12
220
|
};
|
|
13
221
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/placement.ts","../src/SelectionToolbar.tsx","../src/useSelectionToolbar.ts","../src/formats.ts"],"sourcesContent":["export interface SelectionRect {\n /** Horizontal centre of the selection. */\n x: number;\n top: number;\n bottom: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport interface PlaceToolbarInput {\n selection: SelectionRect;\n toolbar: Size;\n viewport: Size;\n /** iOS renders its own callout above a selection, which covers a toolbar\n * placed there, so the caller asks for below on that platform. */\n preferBelow?: boolean;\n}\n\nexport interface ToolbarPlacement {\n left: number;\n top: number;\n below: boolean;\n}\n\nconst GAP = 8;\n\n/**\n * placeToolbar positions a floating toolbar against a selection.\n *\n * Kept apart from the component because it is the part that is wrong in subtle\n * ways — off-screen at the edges, hidden under a system callout — and the part\n * a test can pin down without a browser.\n */\nexport function placeToolbar({\n selection,\n toolbar,\n viewport,\n preferBelow = false,\n}: PlaceToolbarInput): ToolbarPlacement {\n const fitsAbove = selection.top - GAP - toolbar.height >= 0;\n const fitsBelow = selection.bottom + GAP + toolbar.height <= viewport.height;\n\n const below = preferBelow ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;\n\n const top = below\n ? selection.bottom + GAP\n : Math.max(0, selection.top - GAP - toolbar.height);\n\n const centred = selection.x - toolbar.width / 2;\n const left = Math.max(0, Math.min(centred, viewport.width - toolbar.width));\n\n return { left, top, below };\n}\n","import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { placeToolbar, type SelectionRect } from \"./placement\";\n\nexport interface ToolbarAction {\n id: string;\n label: string;\n icon?: React.ReactNode;\n onSelect: (selectedText: string) => void;\n}\n\nexport interface BlockFormat {\n id: string;\n label: string;\n shortcut?: string;\n isActive?: boolean;\n onSelect: () => void;\n}\n\nexport interface SelectionToolbarProps {\n selectedText: string;\n selection: SelectionRect;\n actions?: ToolbarAction[];\n /** Heading levels and list kinds, rendered behind a single trigger. */\n formats?: BlockFormat[];\n formatsLabel?: string;\n /** iOS draws its own callout above a selection; the host detects it. */\n preferBelow?: boolean;\n container?: HTMLElement | null;\n}\n\nconst FALLBACK = { width: 320, height: 40 };\n\nexport function SelectionToolbar({\n selectedText,\n selection,\n actions = [],\n formats = [],\n formatsLabel = \"Format\",\n preferBelow = false,\n container,\n}: SelectionToolbarProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(FALLBACK);\n const [formatsOpen, setFormatsOpen] = useState(false);\n\n // Measured after paint rather than assumed: the toolbar's width depends on\n // labels the host supplies, and guessing it puts the toolbar off-centre.\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n if (rect.width > 0) setSize({ width: rect.width, height: rect.height });\n }, [selectedText, actions.length, formats.length]);\n\n useEffect(() => {\n if (!formatsOpen) return;\n const close = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setFormatsOpen(false);\n };\n document.addEventListener(\"mousedown\", close);\n return () => document.removeEventListener(\"mousedown\", close);\n }, [formatsOpen]);\n\n if (typeof document === \"undefined\") return null;\n\n const place = placeToolbar({\n selection,\n toolbar: size,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n preferBelow,\n });\n\n return createPortal(\n <div\n ref={ref}\n role=\"toolbar\"\n className=\"lalt-toolbar\"\n style={{ position: \"fixed\", left: place.left, top: place.top, zIndex: 100 }}\n >\n {actions.map((action) => (\n <button\n key={action.id}\n type=\"button\"\n className=\"lalt-toolbar__button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => action.onSelect(selectedText)}\n >\n {action.icon}\n <span>{action.label}</span>\n </button>\n ))}\n\n {formats.length > 0 && (\n <div className=\"lalt-toolbar__formats\">\n <button\n type=\"button\"\n className=\"lalt-toolbar__button\"\n aria-haspopup=\"menu\"\n aria-expanded={formatsOpen}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => setFormatsOpen((open) => !open)}\n >\n {formatsLabel}\n </button>\n {formatsOpen && (\n <div role=\"menu\" className=\"lalt-toolbar__menu\">\n {formats.map((format) => (\n <button\n key={format.id}\n type=\"button\"\n role=\"menuitem\"\n className=\"lalt-toolbar__item\"\n aria-current={format.isActive}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n format.onSelect();\n setFormatsOpen(false);\n }}\n >\n <span>{format.label}</span>\n {format.shortcut && (\n <kbd className=\"lalt-toolbar__shortcut\">{format.shortcut}</kbd>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>,\n container ?? document.body,\n );\n}\n","import { useEffect, useState } from \"react\";\nimport type { Editor } from \"@tiptap/react\";\n\nimport type { SelectionRect } from \"./placement\";\n\nexport interface SelectionInfo {\n text: string;\n rect: SelectionRect;\n}\n\n/**\n * useSelectionToolbar reports the current non-empty selection of an editor.\n *\n * Returns null whenever there is nothing to act on, so the caller renders the\n * toolbar only while it has a target.\n */\nexport function useSelectionToolbar(editor: Editor | null): SelectionInfo | null {\n const [selection, setSelection] = useState<SelectionInfo | null>(null);\n\n useEffect(() => {\n if (!editor) return;\n\n const read = () => {\n const { state } = editor;\n const { from, to, empty } = state.selection;\n if (empty || !editor.isFocused) {\n setSelection(null);\n return;\n }\n\n const text = state.doc.textBetween(from, to, \" \").trim();\n if (text === \"\") {\n setSelection(null);\n return;\n }\n\n const start = editor.view.coordsAtPos(from);\n const end = editor.view.coordsAtPos(to);\n setSelection({\n text,\n rect: {\n x: (start.left + end.right) / 2,\n top: Math.min(start.top, end.top),\n bottom: Math.max(start.bottom, end.bottom),\n },\n });\n };\n\n read();\n editor.on(\"selectionUpdate\", read);\n editor.on(\"blur\", read);\n editor.on(\"focus\", read);\n return () => {\n editor.off(\"selectionUpdate\", read);\n editor.off(\"blur\", read);\n editor.off(\"focus\", read);\n };\n }, [editor]);\n\n return selection;\n}\n","import type { Editor } from \"@tiptap/react\";\n\n// Type-only: these commands are contributed by StarterKit's extensions, and\n// without the reference TypeScript does not know they exist on a chain. No\n// runtime import, so the host stays in charge of which extensions it mounts —\n// blockFormats simply returns entries whose commands no-op if one is missing.\nimport type {} from \"@tiptap/starter-kit\";\n\nimport type { BlockFormat } from \"./SelectionToolbar\";\n\nexport interface FormatLabels {\n paragraph: string;\n heading1: string;\n heading2: string;\n heading3: string;\n orderedList: string;\n bulletList: string;\n}\n\nexport const defaultFormatLabels: FormatLabels = {\n paragraph: \"Texte\",\n heading1: \"Titre 1\",\n heading2: \"Titre 2\",\n heading3: \"Titre 3\",\n orderedList: \"Liste numérotée\",\n bulletList: \"Liste à puces\",\n};\n\n/**\n * blockFormats is the set a writer reaches for on a selected passage.\n *\n * Labels are passed in rather than translated here: the package carries no\n * i18n runtime, so the host names things in whatever language it speaks.\n */\nexport function blockFormats(\n editor: Editor | null,\n labels: FormatLabels = defaultFormatLabels,\n): BlockFormat[] {\n if (!editor) return [];\n\n return [\n {\n id: \"paragraph\",\n label: labels.paragraph,\n shortcut: \"Ctrl+Alt+0\",\n isActive: editor.isActive(\"paragraph\"),\n onSelect: () => editor.chain().focus().setParagraph().run(),\n },\n ...([1, 2, 3] as const).map((level) => ({\n id: `heading${level}`,\n label: labels[`heading${level}` as keyof FormatLabels],\n shortcut: `Ctrl+Alt+${level}`,\n isActive: editor.isActive(\"heading\", { level }),\n onSelect: () => editor.chain().focus().toggleHeading({ level }).run(),\n })),\n {\n id: \"orderedList\",\n label: labels.orderedList,\n shortcut: \"Ctrl+Alt+4\",\n isActive: editor.isActive(\"orderedList\"),\n onSelect: () => editor.chain().focus().toggleOrderedList().run(),\n },\n {\n id: \"bulletList\",\n label: labels.bulletList,\n shortcut: \"Ctrl+Alt+5\",\n isActive: editor.isActive(\"bulletList\"),\n onSelect: () => editor.chain().focus().toggleBulletList().run(),\n },\n ];\n}\n"],"mappings":";;;;;;;;AA2BA,IAAM,MAAM;AASL,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAAwC;AACtC,QAAM,YAAY,UAAU,MAAM,MAAM,QAAQ,UAAU;AAC1D,QAAM,YAAY,UAAU,SAAS,MAAM,QAAQ,UAAU,SAAS;AAEtE,QAAM,QAAQ,cAAc,aAAa,CAAC,YAAY,CAAC,aAAa;AAEpE,QAAM,MAAM,QACR,UAAU,SAAS,MACnB,KAAK,IAAI,GAAG,UAAU,MAAM,MAAM,QAAQ,MAAM;AAEpD,QAAM,UAAU,UAAU,IAAI,QAAQ,QAAQ;AAC9C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,SAAS,SAAS,QAAQ,QAAQ,KAAK,CAAC;AAE1E,SAAO,EAAE,MAAM,KAAK,MAAM;AAC5B;;;ACvDA,SAAS,WAAW,iBAAiB,QAAQ,gBAAgB;AAC7D,SAAS,oBAAoB;AAiFrB,SAQE,KARF;AAlDR,IAAM,WAAW,EAAE,OAAO,KAAK,QAAQ,GAAG;AAEnC,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,UAAU,CAAC;AAAA,EACX,UAAU,CAAC;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd;AACF,GAA0B;AACxB,QAAM,MAAM,OAAuB,IAAI;AACvC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,QAAQ;AACzC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAIpD,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,sBAAsB;AACtC,QAAI,KAAK,QAAQ,EAAG,SAAQ,EAAE,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO,CAAC;AAAA,EACxE,GAAG,CAAC,cAAc,QAAQ,QAAQ,QAAQ,MAAM,CAAC;AAEjD,YAAU,MAAM;AACd,QAAI,CAAC,YAAa;AAClB,UAAM,QAAQ,CAAC,MAAkB;AAC/B,UAAI,IAAI,WAAW,CAAC,IAAI,QAAQ,SAAS,EAAE,MAAc,EAAG,gBAAe,KAAK;AAAA,IAClF;AACA,aAAS,iBAAiB,aAAa,KAAK;AAC5C,WAAO,MAAM,SAAS,oBAAoB,aAAa,KAAK;AAAA,EAC9D,GAAG,CAAC,WAAW,CAAC;AAEhB,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,QAAM,QAAQ,aAAa;AAAA,IACzB;AAAA,IACA,SAAS;AAAA,IACT,UAAU,EAAE,OAAO,OAAO,YAAY,QAAQ,OAAO,YAAY;AAAA,IACjE;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,UAAU,SAAS,MAAM,MAAM,MAAM,KAAK,MAAM,KAAK,QAAQ,IAAI;AAAA,QAEzE;AAAA,kBAAQ,IAAI,CAAC,WACZ;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,cACrC,SAAS,MAAM,OAAO,SAAS,YAAY;AAAA,cAE1C;AAAA,uBAAO;AAAA,gBACR,oBAAC,UAAM,iBAAO,OAAM;AAAA;AAAA;AAAA,YAPf,OAAO;AAAA,UAQd,CACD;AAAA,UAEA,QAAQ,SAAS,KAChB,qBAAC,SAAI,WAAU,yBACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,iBAAc;AAAA,gBACd,iBAAe;AAAA,gBACf,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,gBACrC,SAAS,MAAM,eAAe,CAAC,SAAS,CAAC,IAAI;AAAA,gBAE5C;AAAA;AAAA,YACH;AAAA,YACC,eACC,oBAAC,SAAI,MAAK,QAAO,WAAU,sBACxB,kBAAQ,IAAI,CAAC,WACZ;AAAA,cAAC;AAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,gBAAc,OAAO;AAAA,gBACrB,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,gBACrC,SAAS,MAAM;AACb,yBAAO,SAAS;AAChB,iCAAe,KAAK;AAAA,gBACtB;AAAA,gBAEA;AAAA,sCAAC,UAAM,iBAAO,OAAM;AAAA,kBACnB,OAAO,YACN,oBAAC,SAAI,WAAU,0BAA0B,iBAAO,UAAS;AAAA;AAAA;AAAA,cAbtD,OAAO;AAAA,YAed,CACD,GACH;AAAA,aAEJ;AAAA;AAAA;AAAA,IAEJ;AAAA,IACA,aAAa,SAAS;AAAA,EACxB;AACF;;;ACtIA,SAAS,aAAAA,YAAW,YAAAC,iBAAgB;AAgB7B,SAAS,oBAAoB,QAA6C;AAC/E,QAAM,CAAC,WAAW,YAAY,IAAIA,UAA+B,IAAI;AAErE,EAAAD,WAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AAEb,UAAM,OAAO,MAAM;AACjB,YAAM,EAAE,MAAM,IAAI;AAClB,YAAM,EAAE,MAAM,IAAI,MAAM,IAAI,MAAM;AAClC,UAAI,SAAS,CAAC,OAAO,WAAW;AAC9B,qBAAa,IAAI;AACjB;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,IAAI,YAAY,MAAM,IAAI,GAAG,EAAE,KAAK;AACvD,UAAI,SAAS,IAAI;AACf,qBAAa,IAAI;AACjB;AAAA,MACF;AAEA,YAAM,QAAQ,OAAO,KAAK,YAAY,IAAI;AAC1C,YAAM,MAAM,OAAO,KAAK,YAAY,EAAE;AACtC,mBAAa;AAAA,QACX;AAAA,QACA,MAAM;AAAA,UACJ,IAAI,MAAM,OAAO,IAAI,SAAS;AAAA,UAC9B,KAAK,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG;AAAA,UAChC,QAAQ,KAAK,IAAI,MAAM,QAAQ,IAAI,MAAM;AAAA,QAC3C;AAAA,MACF,CAAC;AAAA,IACH;AAEA,SAAK;AACL,WAAO,GAAG,mBAAmB,IAAI;AACjC,WAAO,GAAG,QAAQ,IAAI;AACtB,WAAO,GAAG,SAAS,IAAI;AACvB,WAAO,MAAM;AACX,aAAO,IAAI,mBAAmB,IAAI;AAClC,aAAO,IAAI,QAAQ,IAAI;AACvB,aAAO,IAAI,SAAS,IAAI;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AACT;;;ACzCO,IAAM,sBAAoC;AAAA,EAC/C,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AACd;AAQO,SAAS,aACd,QACA,SAAuB,qBACR;AACf,MAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,SAAO;AAAA,IACL;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,MACV,UAAU,OAAO,SAAS,WAAW;AAAA,MACrC,UAAU,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI;AAAA,IAC5D;AAAA,IACA,GAAI,CAAC,GAAG,GAAG,CAAC,EAAY,IAAI,CAAC,WAAW;AAAA,MACtC,IAAI,UAAU,KAAK;AAAA,MACnB,OAAO,OAAO,UAAU,KAAK,EAAwB;AAAA,MACrD,UAAU,YAAY,KAAK;AAAA,MAC3B,UAAU,OAAO,SAAS,WAAW,EAAE,MAAM,CAAC;AAAA,MAC9C,UAAU,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,IAAI;AAAA,IACtE,EAAE;AAAA,IACF;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,MACV,UAAU,OAAO,SAAS,aAAa;AAAA,MACvC,UAAU,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI;AAAA,IACjE;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,MACV,UAAU,OAAO,SAAS,YAAY;AAAA,MACtC,UAAU,MAAM,OAAO,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI;AAAA,IAChE;AAAA,EACF;AACF;","names":["useEffect","useState"]}
|
package/dist/revisions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BlockChange, RichDoc } from './
|
|
2
|
-
export { ChangeKind } from './
|
|
1
|
+
import { B as BlockChange, R as RichDoc } from './diff-uFiP8hD5.js';
|
|
2
|
+
export { C as ChangeKind } from './diff-uFiP8hD5.js';
|
|
3
3
|
|
|
4
4
|
type Decision = "pending" | "accepted" | "rejected";
|
|
5
5
|
interface RevisionState {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalternative/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared TipTap editor for L'Alternative apps: rich text, format menu, selection toolbar and inline AI revisions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,5 +54,10 @@
|
|
|
54
54
|
"type": "git",
|
|
55
55
|
"url": "https://github.com/lalternative/packages.git",
|
|
56
56
|
"directory": "editor"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@tiptap/starter-kit": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
}
|