@threadlabs/looma 0.29.5 → 0.29.7

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.
@@ -0,0 +1,29 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ /** The code block the selection lies in: its position, saved language (`auto` when none), text, and element. */
3
+ export interface LoomaCodeBlockAtSelection {
4
+ readonly pos: number;
5
+ readonly language: string;
6
+ readonly text: string;
7
+ readonly element: HTMLElement;
8
+ }
9
+ /** The code block holding the whole selection, or null when the selection is elsewhere or spans blocks. */
10
+ export declare function codeBlockAtSelection(editor: Editor): LoomaCodeBlockAtSelection | null;
11
+ /**
12
+ * The picker's choices: Auto, naming the detected language when there is one; a saved language the
13
+ * catalog lacks, marked unavailable, so it stays selected; then every catalog language.
14
+ */
15
+ export declare function codeLanguageChoices(language: string, detected: string | null, names: readonly string[]): Array<{
16
+ value: string;
17
+ label: string;
18
+ }>;
19
+ /** Centers the picker over the code block, above it when there is room and below it otherwise. */
20
+ export declare function codeLanguagePickerPlacement(block: DOMRect): {
21
+ top: string;
22
+ left: string;
23
+ transform: string;
24
+ };
25
+ /**
26
+ * Saves `value` as the language of the code block at `pos`, as its own undo step; `auto` clears it.
27
+ * @failure Returns false, and changes nothing, for a language outside `names` or no code block at `pos`.
28
+ */
29
+ export declare function setCodeBlockLanguage(editor: Editor, pos: number, value: string, names: readonly string[]): boolean;
@@ -53,6 +53,7 @@ export interface ActiveTableUiState {
53
53
  headerColumn: boolean;
54
54
  capabilities: TableActionCapabilities;
55
55
  }
56
+ export declare const NO_TABLE_CAPABILITIES: TableActionCapabilities;
56
57
  /**
57
58
  * Text formatting belongs to actual text selections, not table CellSelections.
58
59
  *
@@ -0,0 +1,35 @@
1
+ import { type ActiveTableUiState, type TableActionCapabilities } from "./extensions/table-commands.js";
2
+ import type { TableCellAlignment, TableCellBackground } from "./extensions/table-formatting.js";
3
+ /** What the table's controls show: the toolbar and overlay, and the selected cells' formatting and actions. */
4
+ export interface LoomaTableControls {
5
+ readonly toolbarOpen: boolean;
6
+ readonly overlayOpen: boolean;
7
+ readonly alignment: TableCellAlignment;
8
+ readonly background: TableCellBackground;
9
+ readonly headerRow: boolean;
10
+ readonly headerColumn: boolean;
11
+ readonly capabilities: TableActionCapabilities;
12
+ }
13
+ export declare const LOOMA_TABLE_CONTROLS_CLOSED: LoomaTableControls;
14
+ /**
15
+ * The controls for the table shown: its overlay on wider screens, and the toolbar and the selection's
16
+ * formatting only while the selection is in that table. A table that is only hovered shows its overlay
17
+ * with nothing selected; narrow screens use the editing dock instead of the toolbar and overlay.
18
+ */
19
+ export declare function tableControls(state: ActiveTableUiState, selectionInTable: boolean, narrow: boolean): LoomaTableControls;
20
+ /**
21
+ * Centers the table toolbar on the table, at most 420px wide and 12px inside the viewport's sides,
22
+ * above the table when there is room and below it otherwise.
23
+ */
24
+ export declare function tableToolbarPlacement(table: DOMRect, viewportWidth: number): {
25
+ top: string;
26
+ left: string;
27
+ maxWidth: string;
28
+ };
29
+ /** The overlay covers the table exactly. */
30
+ export declare const tableOverlayPlacement: (table: DOMRect) => {
31
+ top: string;
32
+ left: string;
33
+ width: string;
34
+ height: string;
35
+ };
@@ -0,0 +1,25 @@
1
+ /** The toolbar's one tooltip: the button it describes, its label and shortcut, and whether it shows. */
2
+ export interface LoomaToolbarTooltipState {
3
+ readonly for: string;
4
+ readonly label: string;
5
+ readonly shortcut: string;
6
+ readonly open: boolean;
7
+ }
8
+ /** How long, in milliseconds, the first button waits before its tooltip shows. */
9
+ export declare const LOOMA_TOOLBAR_TOOLTIP_DELAY = 400;
10
+ /**
11
+ * One tooltip that follows a toolbar row: the first button waits, and moving along the row while it
12
+ * shows is immediate, which the native `title` attribute cannot do.
13
+ *
14
+ * @ownership The host renders the tooltip from the state and calls `show` and `hide` from its buttons.
15
+ * @lifecycle A pending delay runs until `hide()` or `destroy()`.
16
+ */
17
+ export interface LoomaToolbarTooltip {
18
+ readonly state: LoomaToolbarTooltipState;
19
+ /** Describes the button with `id`; `immediate` skips the delay, as keyboard focus does. */
20
+ show(id: string, label: string, immediate: boolean, shortcut?: string): void;
21
+ hide(): void;
22
+ destroy(): void;
23
+ }
24
+ /** Calls `onChange` after every state change. */
25
+ export declare function createToolbarTooltip(onChange: (state: LoomaToolbarTooltipState) => void): LoomaToolbarTooltip;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.29.5",
3
+ "version": "0.29.7",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -384,6 +384,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
384
384
  linkBaseUrl: string;
385
385
  disableHighlight: boolean;
386
386
  codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
387
+ toolbarOpen: boolean;
387
388
  modelValue: JSONContent;
388
389
  slashCommands: LoomaEditorSlashCommands | undefined;
389
390
  mentionQuery: LoomaMentionQuery | undefined;
@@ -401,5 +402,4 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
401
402
  selectionActions: LoomaEditorSelectionAction[];
402
403
  rangeActions: LoomaEditorRangeAction[];
403
404
  toolbarTriggerId: string;
404
- toolbarOpen: boolean;
405
405
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,8 +1,8 @@
1
1
  import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
2
2
  import { Extension, Mark, Node, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
3
3
  import { Editor as Editor$1, EditorContent } from "@tiptap/vue-3";
4
- import { closeHistory, history, redo, undo } from "@tiptap/pm/history";
5
4
  import { Plugin } from "@tiptap/pm/state";
5
+ import { closeHistory, history, redo, undo } from "@tiptap/pm/history";
6
6
  //#region ../../node_modules/.pnpm/@tiptap+extension-document@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-document/dist/index.js
7
7
  /**
8
8
  * The default document node which represents the top level node of the editor.
@@ -3,12 +3,12 @@ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ICONS, createLoomaMentionExtension,
3
3
  import { Button, Callout, Card, Checkbox, Cluster, Disclosure, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, Icon, IconButton, Image, Input, List, Menu, MenuItem, Popover, Separator, Stack, StatusMessage, Text, Tooltip } from "@threadlabs/looma/vue";
4
4
  import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
5
  import { Editor as Editor$1 } from "@tiptap/vue-3";
6
- import { closeHistory } from "@tiptap/pm/history";
7
6
  import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
8
7
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
9
8
  import { createLowlight } from "lowlight";
10
9
  import "@tiptap/pm/tables";
11
10
  import "@tiptap/pm/transform";
11
+ import { closeHistory } from "@tiptap/pm/history";
12
12
  import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
13
13
  export * from "@threadlabs/looma/editor";
14
14
  //#region src/vue/editor/primitives.ts
@@ -1562,6 +1562,23 @@ function selectedTableCellElement(editor) {
1562
1562
  }
1563
1563
  //#endregion
1564
1564
  //#region src/editor/extensions/table-commands.ts
1565
+ var NO_TABLE_CAPABILITIES = {
1566
+ canToggleHeaderRow: false,
1567
+ canToggleHeaderColumn: false,
1568
+ canMoveRowUp: false,
1569
+ canMoveRowDown: false,
1570
+ canMoveColumnLeft: false,
1571
+ canMoveColumnRight: false,
1572
+ canAddRowBefore: false,
1573
+ canAddRowAfter: false,
1574
+ canAddColumnBefore: false,
1575
+ canAddColumnAfter: false,
1576
+ canDeleteRow: false,
1577
+ canDeleteColumn: false,
1578
+ canDeleteTable: false,
1579
+ canMergeCells: false,
1580
+ canSplitCell: false
1581
+ };
1565
1582
  /**
1566
1583
  * Moves the caret to the cell an overlay action names when the action comes from a table other
1567
1584
  * than the selected one, so table commands act on that table. A cell menu also selects its cell,
@@ -1879,6 +1896,59 @@ function selectionActionDetail(state, id) {
1879
1896
  after: doc.textBetween(to, Math.min(doc.content.size, to + CONTEXT), "\n")
1880
1897
  };
1881
1898
  }
1899
+ //#endregion
1900
+ //#region src/editor/table-ui.ts
1901
+ var LOOMA_TABLE_CONTROLS_CLOSED = Object.freeze({
1902
+ toolbarOpen: false,
1903
+ overlayOpen: false,
1904
+ alignment: "left",
1905
+ background: null,
1906
+ headerRow: false,
1907
+ headerColumn: false,
1908
+ capabilities: Object.freeze({ ...NO_TABLE_CAPABILITIES })
1909
+ });
1910
+ /**
1911
+ * The controls for the table shown: its overlay on wider screens, and the toolbar and the selection's
1912
+ * formatting only while the selection is in that table. A table that is only hovered shows its overlay
1913
+ * with nothing selected; narrow screens use the editing dock instead of the toolbar and overlay.
1914
+ */
1915
+ function tableControls(state, selectionInTable, narrow) {
1916
+ if (!selectionInTable) return {
1917
+ ...LOOMA_TABLE_CONTROLS_CLOSED,
1918
+ overlayOpen: !narrow
1919
+ };
1920
+ return {
1921
+ toolbarOpen: state.showToolbar && !narrow,
1922
+ overlayOpen: !narrow,
1923
+ alignment: state.cellAlignment,
1924
+ background: state.cellBackground,
1925
+ headerRow: state.headerRow,
1926
+ headerColumn: state.headerColumn,
1927
+ capabilities: state.capabilities
1928
+ };
1929
+ }
1930
+ /** Space, in pixels, the toolbar needs above a table; with less it sits below. */
1931
+ var ROOM_ABOVE$1 = 76;
1932
+ /**
1933
+ * Centers the table toolbar on the table, at most 420px wide and 12px inside the viewport's sides,
1934
+ * above the table when there is room and below it otherwise.
1935
+ */
1936
+ function tableToolbarPlacement(table, viewportWidth) {
1937
+ const width = Math.min(420, viewportWidth - 24);
1938
+ const left = Math.min(Math.max(12, table.left + (table.width - width) / 2), Math.max(12, viewportWidth - width - 12));
1939
+ return {
1940
+ top: `${table.top > ROOM_ABOVE$1 ? table.top - 52 : table.bottom + 12}px`,
1941
+ left: `${left}px`,
1942
+ maxWidth: `${width}px`
1943
+ };
1944
+ }
1945
+ /** The overlay covers the table exactly. */
1946
+ var tableOverlayPlacement = (table) => ({
1947
+ top: `${table.top}px`,
1948
+ left: `${table.left}px`,
1949
+ width: `${table.width}px`,
1950
+ height: `${table.height}px`
1951
+ });
1882
1952
  /** Whether a viewport this wide gets the editing dock rather than floating tools. */
1883
1953
  var usesMobileDock = (viewportWidth) => viewportWidth <= 767;
1884
1954
  /**
@@ -2439,6 +2509,116 @@ function formatEditorShortcut(keys, apple) {
2439
2509
  ].filter(Boolean).join("+");
2440
2510
  }
2441
2511
  var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
2512
+ /** Calls `onChange` after every state change. */
2513
+ function createToolbarTooltip(onChange) {
2514
+ let state = {
2515
+ for: "",
2516
+ label: "",
2517
+ shortcut: "",
2518
+ open: false
2519
+ };
2520
+ let timer;
2521
+ const set = (patch) => {
2522
+ state = {
2523
+ ...state,
2524
+ ...patch
2525
+ };
2526
+ onChange(state);
2527
+ };
2528
+ const clear = () => {
2529
+ if (timer !== void 0) clearTimeout(timer);
2530
+ timer = void 0;
2531
+ };
2532
+ return {
2533
+ get state() {
2534
+ return state;
2535
+ },
2536
+ show(id, label, immediate, shortcut = "") {
2537
+ clear();
2538
+ const open = immediate || state.open;
2539
+ set({
2540
+ for: id,
2541
+ label,
2542
+ shortcut: shortcut ? formatEditorShortcut(shortcut, isApplePlatform()) : "",
2543
+ open
2544
+ });
2545
+ if (!open) timer = setTimeout(() => {
2546
+ timer = void 0;
2547
+ set({ open: true });
2548
+ }, 400);
2549
+ },
2550
+ hide() {
2551
+ clear();
2552
+ if (state.open) set({ open: false });
2553
+ },
2554
+ destroy: clear
2555
+ };
2556
+ }
2557
+ //#endregion
2558
+ //#region src/editor/code-language-picker.ts
2559
+ /** The code block holding the whole selection, or null when the selection is elsewhere or spans blocks. */
2560
+ function codeBlockAtSelection(editor) {
2561
+ const { $from, $to } = editor.state.selection;
2562
+ const block = $from.parent;
2563
+ if (block.type.name !== "codeBlock" || !$from.sameParent($to)) return null;
2564
+ const pos = $from.before();
2565
+ const element = editor.view.nodeDOM(pos);
2566
+ if (!(element instanceof HTMLElement)) return null;
2567
+ return {
2568
+ pos,
2569
+ language: typeof block.attrs.language === "string" && block.attrs.language ? block.attrs.language : "auto",
2570
+ text: block.textContent,
2571
+ element
2572
+ };
2573
+ }
2574
+ /**
2575
+ * The picker's choices: Auto, naming the detected language when there is one; a saved language the
2576
+ * catalog lacks, marked unavailable, so it stays selected; then every catalog language.
2577
+ */
2578
+ function codeLanguageChoices(language, detected, names) {
2579
+ return [
2580
+ {
2581
+ value: "auto",
2582
+ label: detected ? `Auto (${detected.toUpperCase()})` : "Auto"
2583
+ },
2584
+ ...language !== "auto" && !names.includes(language) ? [{
2585
+ value: language,
2586
+ label: `${language.toUpperCase()} (unavailable)`
2587
+ }] : [],
2588
+ ...names.map((name) => ({
2589
+ value: name,
2590
+ label: name.toUpperCase()
2591
+ }))
2592
+ ];
2593
+ }
2594
+ /** Space, in pixels, the picker needs above a code block; with less it sits below. */
2595
+ var ROOM_ABOVE = 76;
2596
+ /** Centers the picker over the code block, above it when there is room and below it otherwise. */
2597
+ function codeLanguagePickerPlacement(block) {
2598
+ const above = block.top > ROOM_ABOVE;
2599
+ return {
2600
+ top: `${above ? block.top - 8 : block.bottom + 8}px`,
2601
+ left: `${block.left + block.width / 2}px`,
2602
+ transform: `translate(-50%, ${above ? "-100%" : "0"})`
2603
+ };
2604
+ }
2605
+ /**
2606
+ * Saves `value` as the language of the code block at `pos`, as its own undo step; `auto` clears it.
2607
+ * @failure Returns false, and changes nothing, for a language outside `names` or no code block at `pos`.
2608
+ */
2609
+ function setCodeBlockLanguage(editor, pos, value, names) {
2610
+ const language = value === "auto" ? null : value;
2611
+ if (language !== null && !names.includes(language)) return false;
2612
+ const node = editor.state.doc.nodeAt(pos);
2613
+ if (node?.type.name !== "codeBlock") return false;
2614
+ editor.view.dispatch(closeHistory(editor.state.tr));
2615
+ editor.view.dispatch(editor.state.tr.setNodeMarkup(pos, void 0, {
2616
+ ...node.attrs,
2617
+ language
2618
+ }));
2619
+ editor.view.dispatch(closeHistory(editor.state.tr));
2620
+ return true;
2621
+ }
2442
2622
  //#endregion
2443
2623
  //#region src/editor/extensions/expand.ts
2444
2624
  /**
@@ -3344,23 +3524,6 @@ var EMPTY_DOCUMENT = {
3344
3524
  };
3345
3525
  var uploadProgressText = (progress) => progress && progress.total > 0 ? `Uploading image… ${Math.round(Math.min(1, progress.loaded / progress.total) * 100)}%` : "Uploading image…";
3346
3526
  var editorInstanceSequence = 0;
3347
- var EMPTY_CAPABILITIES = {
3348
- canToggleHeaderRow: false,
3349
- canToggleHeaderColumn: false,
3350
- canMoveRowUp: false,
3351
- canMoveRowDown: false,
3352
- canMoveColumnLeft: false,
3353
- canMoveColumnRight: false,
3354
- canAddRowBefore: false,
3355
- canAddRowAfter: false,
3356
- canAddColumnBefore: false,
3357
- canAddColumnAfter: false,
3358
- canDeleteRow: false,
3359
- canDeleteColumn: false,
3360
- canDeleteTable: false,
3361
- canMergeCells: false,
3362
- canSplitCell: false
3363
- };
3364
3527
  function slashMenuRows(items) {
3365
3528
  const groups = [];
3366
3529
  for (const [index, item] of items.entries()) {
@@ -3573,35 +3736,15 @@ var LoomaEditor = defineComponent({
3573
3736
  let chipEditing;
3574
3737
  const chipState = shallowRef(LOOMA_CHIP_EDITING_IDLE);
3575
3738
  const toolbarScope = `looma-editor-tool-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
3576
- const tooltipFor = ref("");
3577
- const tooltipLabel = ref("");
3578
- const tooltipShortcut = ref("");
3579
- const tooltipOpen = ref(false);
3739
+ const toolbarTooltip = createToolbarTooltip((state) => {
3740
+ tooltip.value = state;
3741
+ });
3742
+ const tooltip = shallowRef(toolbarTooltip.state);
3580
3743
  const resizeHintFor = ref("");
3581
3744
  let resizeHintSequence = 0;
3582
- let tooltipTimer = 0;
3583
- const clearTooltipTimer = () => {
3584
- if (tooltipTimer) window.clearTimeout(tooltipTimer);
3585
- tooltipTimer = 0;
3586
- };
3587
- const showTool = (id, label, immediate, shortcut = "") => {
3588
- clearTooltipTimer();
3589
- tooltipFor.value = id;
3590
- tooltipLabel.value = label;
3591
- tooltipShortcut.value = shortcut ? formatEditorShortcut(shortcut, isApplePlatform()) : "";
3592
- if (immediate || tooltipOpen.value) {
3593
- tooltipOpen.value = true;
3594
- return;
3595
- }
3596
- tooltipTimer = window.setTimeout(() => {
3597
- tooltipOpen.value = true;
3598
- }, 400);
3599
- };
3600
- const hideTool = () => {
3601
- clearTooltipTimer();
3602
- tooltipOpen.value = false;
3603
- };
3604
- onBeforeUnmount(clearTooltipTimer);
3745
+ const showTool = toolbarTooltip.show;
3746
+ const hideTool = toolbarTooltip.hide;
3747
+ onBeforeUnmount(toolbarTooltip.destroy);
3605
3748
  const tableToolbarShell = ref(null);
3606
3749
  const tableOverlayShell = ref(null);
3607
3750
  const tableMenuShell = ref(null);
@@ -3670,7 +3813,7 @@ var LoomaEditor = defineComponent({
3670
3813
  toolbarStyle: {},
3671
3814
  overlayStyle: {},
3672
3815
  menuStyle: {},
3673
- capabilities: { ...EMPTY_CAPABILITIES }
3816
+ capabilities: LOOMA_TABLE_CONTROLS_CLOSED.capabilities
3674
3817
  });
3675
3818
  const openChipEditor = (_instance, position) => {
3676
3819
  if (!chipEditing?.open(position)) return;
@@ -4089,16 +4232,10 @@ var LoomaEditor = defineComponent({
4089
4232
  if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
4090
4233
  }, { deep: true });
4091
4234
  const closeTableUi = () => {
4092
- tableUi.toolbarOpen = false;
4093
- tableUi.overlayOpen = false;
4235
+ Object.assign(tableUi, LOOMA_TABLE_CONTROLS_CLOSED);
4094
4236
  tableUi.menuOpen = false;
4095
4237
  tableUi.menuScope = "cell";
4096
- tableUi.alignment = "left";
4097
- tableUi.background = null;
4098
- tableUi.headerRow = false;
4099
- tableUi.headerColumn = false;
4100
4238
  tableUi.geometry = null;
4101
- tableUi.capabilities = { ...EMPTY_CAPABILITIES };
4102
4239
  overlayTableElement = null;
4103
4240
  mobileDock.tableInactive();
4104
4241
  };
@@ -4120,63 +4257,31 @@ var LoomaEditor = defineComponent({
4120
4257
  let codeBlurTimer;
4121
4258
  const updateCodeUi = (focusSettled = false) => {
4122
4259
  const instance = editor.value?.isDestroyed ? void 0 : editor.value;
4123
- const selection = instance?.state.selection;
4124
- const block = selection?.$from.parent;
4125
- const dom = instance && block?.type.name === "codeBlock" && selection.$from.sameParent(selection.$to) ? instance.view.nodeDOM(selection.$from.before()) : null;
4126
- if (!instance || !props.editable || !codeLanguageNames.length || !(dom instanceof HTMLElement)) {
4260
+ const block = instance ? codeBlockAtSelection(instance) : null;
4261
+ if (!instance || !props.editable || !codeLanguageNames.length || !block) {
4127
4262
  codeUi.open = false;
4128
4263
  return;
4129
4264
  }
4130
- loadForCodeBlock(instance, block.attrs.language, block.textContent);
4265
+ loadForCodeBlock(instance, block.language === "auto" ? null : block.language, block.text);
4131
4266
  if (!instance.isFocused && !codeLanguageShell.value?.contains(document.activeElement)) {
4132
4267
  clearTimeout(codeBlurTimer);
4133
4268
  if (codeUi.open && !focusSettled) codeBlurTimer = setTimeout(() => updateCodeUi(true), 0);
4134
4269
  else codeUi.open = false;
4135
4270
  return;
4136
4271
  }
4137
- const rect = dom.getBoundingClientRect();
4138
- const above = rect.top > 76;
4139
4272
  codeUi.open = true;
4140
- codeUi.pos = selection.$from.before();
4141
- codeUi.language = typeof block.attrs.language === "string" && block.attrs.language ? block.attrs.language : "auto";
4142
- codeUi.text = block.textContent;
4143
- codeUi.style = {
4144
- top: `${above ? rect.top - 8 : rect.bottom + 8}px`,
4145
- left: `${rect.left + rect.width / 2}px`,
4146
- transform: `translate(-50%, ${above ? "-100%" : "0"})`
4147
- };
4273
+ codeUi.pos = block.pos;
4274
+ codeUi.language = block.language;
4275
+ codeUi.text = block.text;
4276
+ codeUi.style = codeLanguagePickerPlacement(block.element.getBoundingClientRect());
4148
4277
  };
4149
4278
  const codeLanguageOptions = computed(() => {
4150
4279
  codeUi.grammarVersion;
4151
4280
  const detected = codeUi.open && codeUi.text.trim() ? codeHighlighting.lowlight.highlightAuto(codeUi.text).data?.language : null;
4152
- return [
4153
- {
4154
- value: "auto",
4155
- label: typeof detected === "string" ? `Auto (${detected.toUpperCase()})` : "Auto"
4156
- },
4157
- ...codeUi.language !== "auto" && !codeLanguageNames.includes(codeUi.language) ? [{
4158
- value: codeUi.language,
4159
- label: `${codeUi.language.toUpperCase()} (unavailable)`
4160
- }] : [],
4161
- ...codeLanguageNames.map((name) => ({
4162
- value: name,
4163
- label: name.toUpperCase()
4164
- }))
4165
- ];
4281
+ return codeLanguageChoices(codeUi.language, typeof detected === "string" ? detected : null, codeLanguageNames);
4166
4282
  });
4167
4283
  const chooseCodeLanguage = (value) => {
4168
- const instance = editor.value;
4169
- if (!instance) return;
4170
- const language = value === "auto" ? null : value;
4171
- if (language !== null && !codeLanguageNames.includes(language)) return;
4172
- const node = instance.state.doc.nodeAt(codeUi.pos);
4173
- if (node?.type.name !== "codeBlock") return;
4174
- instance.view.dispatch(closeHistory(instance.state.tr));
4175
- instance.view.dispatch(instance.state.tr.setNodeMarkup(codeUi.pos, void 0, {
4176
- ...node.attrs,
4177
- language
4178
- }));
4179
- instance.view.dispatch(closeHistory(instance.state.tr));
4284
+ if (editor.value) setCodeBlockLanguage(editor.value, codeUi.pos, value, codeLanguageNames);
4180
4285
  };
4181
4286
  const onCodeTransaction = () => updateCodeUi();
4182
4287
  watch(() => props.editable, () => nextTick(onCodeTransaction));
@@ -4205,31 +4310,14 @@ var LoomaEditor = defineComponent({
4205
4310
  overlayTableElement = table;
4206
4311
  if (selectedTableIsActive) mobileDock.tableActive();
4207
4312
  const rect = table.getBoundingClientRect();
4208
- const maxToolbarWidth = Math.min(420, window.innerWidth - 24);
4209
- const toolbarLeft = Math.min(Math.max(12, rect.left + (rect.width - maxToolbarWidth) / 2), Math.max(12, window.innerWidth - maxToolbarWidth - 12));
4210
- tableUi.toolbarOpen = selectedTableIsActive && state.showToolbar && !mobile.value;
4211
- tableUi.overlayOpen = !mobile.value;
4212
- tableUi.alignment = selectedTableIsActive ? state.cellAlignment : "left";
4213
- tableUi.background = selectedTableIsActive ? state.cellBackground : null;
4214
- tableUi.headerRow = selectedTableIsActive && state.headerRow;
4215
- tableUi.headerColumn = selectedTableIsActive && state.headerColumn;
4216
- tableUi.capabilities = selectedTableIsActive ? state.capabilities : { ...EMPTY_CAPABILITIES };
4313
+ Object.assign(tableUi, tableControls(state, selectedTableIsActive, mobile.value));
4217
4314
  const selectedCell = selectedTableIsActive ? selectedTableCellElement(instance) : null;
4218
4315
  const geometry = measureTableOverlayGeometry(table, selectedCell, hoveredTableCell && table.contains(hoveredTableCell) ? hoveredTableCell : null);
4219
4316
  tableUi.rows = Math.max(0, geometry.rowBoundaries.length - 1);
4220
4317
  tableUi.cols = Math.max(0, geometry.columnBoundaries.length - 1);
4221
4318
  tableUi.geometry = geometry;
4222
- tableUi.toolbarStyle = {
4223
- top: `${rect.top > 76 ? rect.top - 52 : rect.bottom + 12}px`,
4224
- left: `${toolbarLeft}px`,
4225
- maxWidth: `${maxToolbarWidth}px`
4226
- };
4227
- tableUi.overlayStyle = {
4228
- top: `${rect.top}px`,
4229
- left: `${rect.left}px`,
4230
- width: `${rect.width}px`,
4231
- height: `${rect.height}px`
4232
- };
4319
+ tableUi.toolbarStyle = tableToolbarPlacement(rect, window.innerWidth);
4320
+ tableUi.overlayStyle = tableOverlayPlacement(rect);
4233
4321
  };
4234
4322
  const onEditorFocus = () => {
4235
4323
  editorFocused.value = true;
@@ -4810,13 +4898,13 @@ var LoomaEditor = defineComponent({
4810
4898
  const actions = full ? [] : renderSelectionActions(instance);
4811
4899
  return h(EditorToolbar, { floating }, () => [...mobile.value ? [...actions, ...buttons] : [...buttons, ...actions], h(Tooltip, {
4812
4900
  class: "looma-editor__toolbar-tip",
4813
- for: tooltipFor.value,
4814
- open: tooltipOpen.value,
4901
+ for: tooltip.value.for,
4902
+ open: tooltip.value.open,
4815
4903
  placement: floating ? "top" : "bottom",
4816
4904
  trigger: "focus"
4817
4905
  }, {
4818
- default: () => tooltipLabel.value,
4819
- shortcut: () => tooltipShortcut.value ? h("kbd", tooltipShortcut.value) : null
4906
+ default: () => tooltip.value.label,
4907
+ shortcut: () => tooltip.value.shortcut ? h("kbd", tooltip.value.shortcut) : null
4820
4908
  })]);
4821
4909
  };
4822
4910
  const focus = (position = "start") => {