@threadlabs/looma 0.29.2 → 0.29.3
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/editor/host-actions.d.ts +47 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +2 -27
- package/vue/editor/index.js +78 -48
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EditorState } from "@tiptap/pm/state";
|
|
2
|
+
/** Host-owned non-mutating commands for the selected passage. */
|
|
3
|
+
export interface LoomaEditorSelectionAction {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
/** Host action available at a collapsed caret inside an ephemeral editor range. */
|
|
8
|
+
export interface LoomaEditorRangeAction extends LoomaEditorSelectionAction {
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
}
|
|
12
|
+
/** Matching ranges are grouped into one control for each action ID. */
|
|
13
|
+
export interface LoomaEditorRangeActionDetail {
|
|
14
|
+
id: string;
|
|
15
|
+
ranges: {
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/** Exact editor range captured before focus leaves for a host action. */
|
|
21
|
+
export interface LoomaEditorSelectionActionDetail {
|
|
22
|
+
id: string;
|
|
23
|
+
from: number;
|
|
24
|
+
to: number;
|
|
25
|
+
text: string;
|
|
26
|
+
before: string;
|
|
27
|
+
after: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The range actions whose range holds the collapsed text caret.
|
|
31
|
+
* @contract Ranges that are not whole numbers, are empty, or fall outside the document are ignored.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rangeActionsAtCaret(state: EditorState, actions: readonly LoomaEditorRangeAction[]): LoomaEditorRangeAction[];
|
|
34
|
+
/** One control per action ID: its label and each distinct range it covers at the caret, in order. */
|
|
35
|
+
export declare function groupRangeActions(actions: readonly LoomaEditorRangeAction[]): Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
detail: LoomaEditorRangeActionDetail;
|
|
39
|
+
}>;
|
|
40
|
+
/** Whether the selection is a passage that selection actions apply to: nonempty text, or everything. */
|
|
41
|
+
export declare function selectsPassage(state: EditorState): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* A selection action's detail for the current passage: its range, its text, and up to 48 characters
|
|
44
|
+
* of context on each side, captured before focus leaves the editor.
|
|
45
|
+
* @failure Returns null when the selection is empty.
|
|
46
|
+
*/
|
|
47
|
+
export declare function selectionActionDetail(state: EditorState, id: string): LoomaEditorSelectionActionDetail | null;
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaCodeLanguages
|
|
|
6
6
|
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
7
|
import { type LoomaImageUploadDetail } from "../../editor/image-uploads.js";
|
|
8
8
|
import { type LoomaResponder } from "../../editor/requests.js";
|
|
9
|
+
import { type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail } from "../../editor/host-actions.js";
|
|
9
10
|
import { type LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
10
11
|
export type { LoomaImageUploadDetail } from "../../editor/image-uploads.js";
|
|
11
12
|
export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js";
|
|
@@ -17,33 +18,7 @@ export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js"
|
|
|
17
18
|
* set. Narrow viewports retain a dock; document commands/content are unchanged.
|
|
18
19
|
*/
|
|
19
20
|
export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover" | "contextual";
|
|
20
|
-
|
|
21
|
-
export interface LoomaEditorSelectionAction {
|
|
22
|
-
id: string;
|
|
23
|
-
label: string;
|
|
24
|
-
}
|
|
25
|
-
/** Host action available at a collapsed caret inside an ephemeral editor range. */
|
|
26
|
-
export interface LoomaEditorRangeAction extends LoomaEditorSelectionAction {
|
|
27
|
-
from: number;
|
|
28
|
-
to: number;
|
|
29
|
-
}
|
|
30
|
-
/** Matching ranges are grouped into one control for each action ID. */
|
|
31
|
-
export interface LoomaEditorRangeActionDetail {
|
|
32
|
-
id: string;
|
|
33
|
-
ranges: {
|
|
34
|
-
from: number;
|
|
35
|
-
to: number;
|
|
36
|
-
}[];
|
|
37
|
-
}
|
|
38
|
-
/** Exact editor range captured before focus leaves for a host action. */
|
|
39
|
-
export interface LoomaEditorSelectionActionDetail {
|
|
40
|
-
id: string;
|
|
41
|
-
from: number;
|
|
42
|
-
to: number;
|
|
43
|
-
text: string;
|
|
44
|
-
before: string;
|
|
45
|
-
after: string;
|
|
46
|
-
}
|
|
21
|
+
export type { LoomaEditorRangeAction, LoomaEditorRangeActionDetail, LoomaEditorSelectionAction, LoomaEditorSelectionActionDetail, } from "../../editor/host-actions.js";
|
|
47
22
|
export type { LoomaLinkTarget } from "../../editor/link-editing.js";
|
|
48
23
|
/** The `slash-commands` request's detail: this editor's default commands. */
|
|
49
24
|
export interface LoomaSlashCommandsDetail {
|
package/vue/editor/index.js
CHANGED
|
@@ -1751,6 +1751,66 @@ function createChipEditing(options) {
|
|
|
1751
1751
|
};
|
|
1752
1752
|
}
|
|
1753
1753
|
//#endregion
|
|
1754
|
+
//#region src/editor/host-actions.ts
|
|
1755
|
+
/** Characters of context on each side of a selected passage. */
|
|
1756
|
+
var CONTEXT = 48;
|
|
1757
|
+
/**
|
|
1758
|
+
* The range actions whose range holds the collapsed text caret.
|
|
1759
|
+
* @contract Ranges that are not whole numbers, are empty, or fall outside the document are ignored.
|
|
1760
|
+
*/
|
|
1761
|
+
function rangeActionsAtCaret(state, actions) {
|
|
1762
|
+
const selection = state.selection;
|
|
1763
|
+
if (!(selection instanceof TextSelection) || !selection.empty) return [];
|
|
1764
|
+
const size = state.doc.content.size;
|
|
1765
|
+
return actions.filter(({ from, to }) => Number.isSafeInteger(from) && Number.isSafeInteger(to) && from >= 0 && to > from && to <= size && from <= selection.from && selection.from <= to);
|
|
1766
|
+
}
|
|
1767
|
+
/** One control per action ID: its label and each distinct range it covers at the caret, in order. */
|
|
1768
|
+
function groupRangeActions(actions) {
|
|
1769
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1770
|
+
for (const { id, label, from, to } of actions) {
|
|
1771
|
+
const group = groups.get(id) ?? {
|
|
1772
|
+
label,
|
|
1773
|
+
ranges: /* @__PURE__ */ new Map()
|
|
1774
|
+
};
|
|
1775
|
+
group.ranges.set(`${from}:${to}`, {
|
|
1776
|
+
from,
|
|
1777
|
+
to
|
|
1778
|
+
});
|
|
1779
|
+
groups.set(id, group);
|
|
1780
|
+
}
|
|
1781
|
+
return [...groups].map(([id, { label, ranges }]) => ({
|
|
1782
|
+
id,
|
|
1783
|
+
label,
|
|
1784
|
+
detail: {
|
|
1785
|
+
id,
|
|
1786
|
+
ranges: [...ranges.values()]
|
|
1787
|
+
}
|
|
1788
|
+
}));
|
|
1789
|
+
}
|
|
1790
|
+
/** Whether the selection is a passage that selection actions apply to: nonempty text, or everything. */
|
|
1791
|
+
function selectsPassage(state) {
|
|
1792
|
+
const selection = state.selection;
|
|
1793
|
+
return !selection.empty && (selection instanceof TextSelection || selection instanceof AllSelection);
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* A selection action's detail for the current passage: its range, its text, and up to 48 characters
|
|
1797
|
+
* of context on each side, captured before focus leaves the editor.
|
|
1798
|
+
* @failure Returns null when the selection is empty.
|
|
1799
|
+
*/
|
|
1800
|
+
function selectionActionDetail(state, id) {
|
|
1801
|
+
const { from, to } = state.selection;
|
|
1802
|
+
if (from === to) return null;
|
|
1803
|
+
const doc = state.doc;
|
|
1804
|
+
return {
|
|
1805
|
+
id,
|
|
1806
|
+
from,
|
|
1807
|
+
to,
|
|
1808
|
+
text: doc.textBetween(from, to, "\n"),
|
|
1809
|
+
before: doc.textBetween(Math.max(0, from - CONTEXT), from, "\n"),
|
|
1810
|
+
after: doc.textBetween(to, Math.min(doc.content.size, to + CONTEXT), "\n")
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
//#endregion
|
|
1754
1814
|
//#region src/editor/extensions/relative-links.ts
|
|
1755
1815
|
/** Keep other origins and non-web schemes intact; same-origin links retain path, query, and fragment. */
|
|
1756
1816
|
function siteRelativeHref(href, baseUrl) {
|
|
@@ -3456,11 +3516,7 @@ var LoomaEditor = defineComponent({
|
|
|
3456
3516
|
let imagePopup = null;
|
|
3457
3517
|
const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
|
|
3458
3518
|
const formattingPickerOpen = () => linkState.value.surface === "popover" || tablePickerOpen.value || formatOpen.value;
|
|
3459
|
-
const activeRangeActions = (instance) =>
|
|
3460
|
-
const selection = instance.state.selection;
|
|
3461
|
-
if (!(selection instanceof TextSelection) || !selection.empty) return [];
|
|
3462
|
-
return props.rangeActions.filter((action) => Number.isSafeInteger(action.from) && Number.isSafeInteger(action.to) && action.from >= 0 && action.to > action.from && action.to <= instance.state.doc.content.size && action.from <= selection.from && selection.from <= action.to);
|
|
3463
|
-
};
|
|
3519
|
+
const activeRangeActions = (instance) => rangeActionsAtCaret(instance.state, props.rangeActions);
|
|
3464
3520
|
const formattingActive = () => formatting?.active() ?? false;
|
|
3465
3521
|
const editorStateVersion = ref(0);
|
|
3466
3522
|
const mobileToolbarMode = ref("formatting");
|
|
@@ -4514,40 +4570,22 @@ var LoomaEditor = defineComponent({
|
|
|
4514
4570
|
onClick: run
|
|
4515
4571
|
}, () => loomaIcon(icon));
|
|
4516
4572
|
};
|
|
4517
|
-
const renderRangeActions = (instance) => {
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
groups.set(action.id, group);
|
|
4573
|
+
const renderRangeActions = (instance) => groupRangeActions(activeRangeActions(instance)).map(({ id, label, detail }) => h(Button, {
|
|
4574
|
+
key: id,
|
|
4575
|
+
"aria-label": label,
|
|
4576
|
+
type: "button",
|
|
4577
|
+
variant: "ghost",
|
|
4578
|
+
tone: "neutral",
|
|
4579
|
+
size: "sm",
|
|
4580
|
+
onPointerdown: (event) => event.preventDefault(),
|
|
4581
|
+
onClick: () => {
|
|
4582
|
+
emit("rangeAction", detail);
|
|
4583
|
+
formattingPopup?.hide();
|
|
4529
4584
|
}
|
|
4530
|
-
|
|
4531
|
-
key: id,
|
|
4532
|
-
"aria-label": group.label,
|
|
4533
|
-
type: "button",
|
|
4534
|
-
variant: "ghost",
|
|
4535
|
-
tone: "neutral",
|
|
4536
|
-
size: "sm",
|
|
4537
|
-
onPointerdown: (event) => event.preventDefault(),
|
|
4538
|
-
onClick: () => {
|
|
4539
|
-
emit("rangeAction", {
|
|
4540
|
-
id,
|
|
4541
|
-
ranges: [...group.ranges.values()]
|
|
4542
|
-
});
|
|
4543
|
-
formattingPopup?.hide();
|
|
4544
|
-
}
|
|
4545
|
-
}, () => group.label));
|
|
4546
|
-
};
|
|
4585
|
+
}, () => label));
|
|
4547
4586
|
const renderSelectionActions = (instance) => {
|
|
4548
|
-
|
|
4549
|
-
if (
|
|
4550
|
-
if (!(selection instanceof TextSelection || selection instanceof AllSelection)) return [];
|
|
4587
|
+
if (instance.state.selection.empty) return renderRangeActions(instance);
|
|
4588
|
+
if (!selectsPassage(instance.state)) return [];
|
|
4551
4589
|
return props.selectionActions.map((action) => h(Button, {
|
|
4552
4590
|
key: action.id,
|
|
4553
4591
|
"aria-label": action.label,
|
|
@@ -4557,17 +4595,9 @@ var LoomaEditor = defineComponent({
|
|
|
4557
4595
|
size: "sm",
|
|
4558
4596
|
onPointerdown: (event) => event.preventDefault(),
|
|
4559
4597
|
onClick: () => {
|
|
4560
|
-
const
|
|
4561
|
-
if (
|
|
4562
|
-
|
|
4563
|
-
emit("selectionAction", {
|
|
4564
|
-
id: action.id,
|
|
4565
|
-
from,
|
|
4566
|
-
to,
|
|
4567
|
-
text: doc.textBetween(from, to, "\n"),
|
|
4568
|
-
before: doc.textBetween(Math.max(0, from - 48), from, "\n"),
|
|
4569
|
-
after: doc.textBetween(to, Math.min(doc.content.size, to + 48), "\n")
|
|
4570
|
-
});
|
|
4598
|
+
const detail = selectionActionDetail(instance.state, action.id);
|
|
4599
|
+
if (!detail) return;
|
|
4600
|
+
emit("selectionAction", detail);
|
|
4571
4601
|
formattingPopup?.hide();
|
|
4572
4602
|
}
|
|
4573
4603
|
}, () => action.label));
|