@threadlabs/looma 0.26.9 → 0.26.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.26.9",
3
+ "version": "0.26.10",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -34,6 +34,19 @@ export interface LoomaEditorSelectionAction {
34
34
  id: string;
35
35
  label: string;
36
36
  }
37
+ /** Host action available at a collapsed caret inside an ephemeral editor range. */
38
+ export interface LoomaEditorRangeAction extends LoomaEditorSelectionAction {
39
+ from: number;
40
+ to: number;
41
+ }
42
+ /** Matching ranges are grouped into one control for each action ID. */
43
+ export interface LoomaEditorRangeActionDetail {
44
+ id: string;
45
+ ranges: {
46
+ from: number;
47
+ to: number;
48
+ }[];
49
+ }
37
50
  /** Exact editor range captured before focus leaves for a host action. */
38
51
  export interface LoomaEditorSelectionActionDetail {
39
52
  id: string;
@@ -184,6 +197,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
184
197
  type: PropType<LoomaEditorSelectionAction[]>;
185
198
  default: () => never[];
186
199
  };
200
+ /** Opt-in caret actions; the host owns durable identities and recalculates positions after edits. */
201
+ rangeActions: {
202
+ type: PropType<LoomaEditorRangeAction[]>;
203
+ default: () => never[];
204
+ };
187
205
  toolbarTriggerId: {
188
206
  type: StringConstructor;
189
207
  default: string;
@@ -209,6 +227,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
209
227
  update: (_value: JSONContent) => true;
210
228
  ready: (_editor: Editor) => true;
211
229
  selectionAction: (_detail: LoomaEditorSelectionActionDetail) => true;
230
+ rangeAction: (_detail: LoomaEditorRangeActionDetail) => true;
212
231
  uploadError: (_error: unknown, _file: File) => true;
213
232
  imageActivate: (_detail: LoomaImageActivationDetail) => true;
214
233
  imageRenditionError: (_detail: LoomaImageRenditionErrorDetail) => true;
@@ -316,6 +335,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
316
335
  type: PropType<LoomaEditorSelectionAction[]>;
317
336
  default: () => never[];
318
337
  };
338
+ /** Opt-in caret actions; the host owns durable identities and recalculates positions after edits. */
339
+ rangeActions: {
340
+ type: PropType<LoomaEditorRangeAction[]>;
341
+ default: () => never[];
342
+ };
319
343
  toolbarTriggerId: {
320
344
  type: StringConstructor;
321
345
  default: string;
@@ -339,6 +363,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
339
363
  "onUpdate:toolbarOpen"?: ((_value: boolean) => any) | undefined;
340
364
  onReady?: ((_editor: Editor) => any) | undefined;
341
365
  onSelectionAction?: ((_detail: LoomaEditorSelectionActionDetail) => any) | undefined;
366
+ onRangeAction?: ((_detail: LoomaEditorRangeActionDetail) => any) | undefined;
342
367
  onUploadError?: ((_error: unknown, _file: File) => any) | undefined;
343
368
  onImageActivate?: ((_detail: LoomaImageActivationDetail) => any) | undefined;
344
369
  onImageRenditionError?: ((_detail: LoomaImageRenditionErrorDetail) => any) | undefined;
@@ -364,6 +389,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
364
389
  toolbarMode: LoomaEditorToolbarMode;
365
390
  compactToolbar: boolean;
366
391
  selectionActions: LoomaEditorSelectionAction[];
392
+ rangeActions: LoomaEditorRangeAction[];
367
393
  toolbarTriggerId: string;
368
394
  toolbarOpen: boolean;
369
395
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,4 +1,4 @@
1
1
  export * from "@threadlabs/looma/editor";
2
2
  export * from "./primitives.js";
3
- export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
3
+ export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
4
4
  export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "./image-delivery.js";
@@ -1710,6 +1710,11 @@ var LoomaEditor = defineComponent({
1710
1710
  type: Array,
1711
1711
  default: () => []
1712
1712
  },
1713
+ /** Opt-in caret actions; the host owns durable identities and recalculates positions after edits. */
1714
+ rangeActions: {
1715
+ type: Array,
1716
+ default: () => []
1717
+ },
1713
1718
  toolbarTriggerId: {
1714
1719
  type: String,
1715
1720
  default: ""
@@ -1734,6 +1739,7 @@ var LoomaEditor = defineComponent({
1734
1739
  update: (_value) => true,
1735
1740
  ready: (_editor) => true,
1736
1741
  selectionAction: (_detail) => true,
1742
+ rangeAction: (_detail) => true,
1737
1743
  uploadError: (_error, _file) => true,
1738
1744
  imageActivate: (_detail) => true,
1739
1745
  imageRenditionError: (_detail) => true
@@ -1825,9 +1831,14 @@ var LoomaEditor = defineComponent({
1825
1831
  heldPressTimer = void 0;
1826
1832
  heldPress = null;
1827
1833
  };
1834
+ const activeRangeActions = (instance) => {
1835
+ const selection = instance.state.selection;
1836
+ if (!(selection instanceof TextSelection) || !selection.empty) return [];
1837
+ 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);
1838
+ };
1828
1839
  const formattingSelectionActive = (instance) => {
1829
1840
  const selection = instance.state.selection;
1830
- return (selection instanceof TextSelection || selection instanceof AllSelection && Boolean(instance.state.doc.textContent)) && !dismissedFormattingSelection?.eq(selection) && (!selection.empty || Boolean(heldFormattingSelection?.eq(selection)));
1841
+ return (selection instanceof TextSelection || selection instanceof AllSelection && Boolean(instance.state.doc.textContent)) && !dismissedFormattingSelection?.eq(selection) && (!selection.empty || Boolean(heldFormattingSelection?.eq(selection)) || activeRangeActions(instance).length > 0);
1831
1842
  };
1832
1843
  const editorStateVersion = ref(0);
1833
1844
  const mobileToolbarMode = ref("formatting");
@@ -2501,6 +2512,10 @@ var LoomaEditor = defineComponent({
2501
2512
  heldFormattingSelection = null;
2502
2513
  formattingPopup?.hide();
2503
2514
  });
2515
+ watch(() => props.rangeActions, () => {
2516
+ const instance = editor.value;
2517
+ if (instance) instance.view.dispatch(instance.state.tr.setMeta("loomaRangeActionsChanged", true));
2518
+ });
2504
2519
  watch(() => props.label, (label) => {
2505
2520
  editor.value?.setOptions({ editorProps: { attributes: {
2506
2521
  role: "textbox",
@@ -3054,9 +3069,40 @@ var LoomaEditor = defineComponent({
3054
3069
  onClick: run
3055
3070
  }, () => loomaIcon(icon));
3056
3071
  };
3072
+ const renderRangeActions = (instance) => {
3073
+ const groups = /* @__PURE__ */ new Map();
3074
+ for (const action of activeRangeActions(instance)) {
3075
+ const group = groups.get(action.id) ?? {
3076
+ label: action.label,
3077
+ ranges: /* @__PURE__ */ new Map()
3078
+ };
3079
+ group.ranges.set(`${action.from}:${action.to}`, {
3080
+ from: action.from,
3081
+ to: action.to
3082
+ });
3083
+ groups.set(action.id, group);
3084
+ }
3085
+ return [...groups].map(([id, group]) => h(Button, {
3086
+ key: id,
3087
+ "aria-label": group.label,
3088
+ type: "button",
3089
+ variant: "ghost",
3090
+ tone: "neutral",
3091
+ size: "sm",
3092
+ onPointerdown: (event) => event.preventDefault(),
3093
+ onClick: () => {
3094
+ emit("rangeAction", {
3095
+ id,
3096
+ ranges: [...group.ranges.values()]
3097
+ });
3098
+ formattingPopup?.hide();
3099
+ }
3100
+ }, () => group.label));
3101
+ };
3057
3102
  const renderSelectionActions = (instance) => {
3058
3103
  const selection = instance.state.selection;
3059
- if (selection.empty || !(selection instanceof TextSelection || selection instanceof AllSelection)) return [];
3104
+ if (selection.empty) return renderRangeActions(instance);
3105
+ if (!(selection instanceof TextSelection || selection instanceof AllSelection)) return [];
3060
3106
  return props.selectionActions.map((action) => h(Button, {
3061
3107
  key: action.id,
3062
3108
  "aria-label": action.label,
@@ -3277,11 +3323,11 @@ var LoomaEditor = defineComponent({
3277
3323
  placement: "top",
3278
3324
  inverse: true
3279
3325
  }, () => "Drag to resize column") : null,
3280
- instance && (props.editable || props.selectionActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3326
+ instance && (props.editable || props.selectionActions.length || props.rangeActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3281
3327
  editor: instance,
3282
3328
  pluginKey: "looma-text-formatting-menu",
3283
3329
  updateDelay: 0,
3284
- shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (!props.editable && props.selectionActions.length > 0 || menuEditor.isFocused || formattingPopup?.popper.contains(document.activeElement) || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
3330
+ shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (!props.editable && (props.selectionActions.length > 0 || props.rangeActions.length > 0) || menuEditor.isFocused || formattingPopup?.popper.contains(document.activeElement) || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
3285
3331
  tippyOptions: {
3286
3332
  onCreate: (popup) => {
3287
3333
  formattingPopup = popup;
@@ -3364,7 +3410,7 @@ var LoomaEditor = defineComponent({
3364
3410
  value: codeUi.language,
3365
3411
  onValueChange: (event) => chooseCodeLanguage(event.detail)
3366
3412
  }, () => codeLanguageOptions.value.map((option) => h("option", { value: option.value }, option.label)))]) : null,
3367
- instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty) && mobile.value && (editorFocused.value || linkOpen.value || !props.editable) ? h("div", {
3413
+ instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty || activeRangeActions(instance).length > 0) && mobile.value && (editorFocused.value || linkOpen.value || !props.editable) ? h("div", {
3368
3414
  ref: mobileToolbarShell,
3369
3415
  class: "looma-editor__mobile-toolbar-shell",
3370
3416
  "data-mode": mobileToolbarMode.value,