@ecsplico/editor 2.1.1 → 2.1.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/block-ui/block-actions.d.ts +21 -0
- package/dist/features/block-ui/block-actions.js +25 -1
- package/dist/features/block-ui/block-actions.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.js +33 -4
- package/dist/features/block-ui/components/BlockActionsOverlay.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.vue.d.ts +4 -0
- package/dist/features/block-ui/index.d.ts +2 -2
- package/dist/features/block-ui/index.js +2 -2
- package/dist/features/block-ui/react/BlockActionsOverlay.d.ts +4 -0
- package/dist/features/block-ui/react/BlockActionsOverlay.js +38 -12
- package/dist/features/block-ui/react/BlockActionsOverlay.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +4 -4
- package/dist/tree/tree-host.js +1 -1
- package/dist/tree/tree-items.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/docs/integration.md +3 -1
- package/docs/llms.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `BLOCK_CORNER_ATTR` (`data-block-corner`), `blockCornerOf(dom)` and the type `BlockCorner` from `/block-ui`: a block marks the icons in its top-right corner with the attribute, and the hover action bar (Vue and React `BlockActionsOverlay`) docks immediately left of them, vertically centred, instead of covering them. Blocks without marked elements keep the bar at the editor's right edge.
|
|
8
|
+
|
|
3
9
|
## 2.1.1
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Every entry is an explicit subpath of `package.json#exports`; there is no wildca
|
|
|
132
132
|
| `@ecsplico/editor/integrations/composite-media` | Framework-free: `compositeMediaFeature` (requires `composites`, `media`): imported files become `document` composites |
|
|
133
133
|
| `@ecsplico/editor/integrations/transcript-ai`, `/transcript-ai/protocol` | Framework-free: `transcriptAiFeature` (requires `transcript`, `ai`), `createTranscriptCleanup`, `hasTranscriptSegments`; server contract: `TRANSCRIPT_CLEANUP_LIMITS`, `transcriptCleanupRequestSchema`, `transcriptCleanupMessages`, `transcriptCleanupChunks`, `parseTranscriptCleanup` |
|
|
134
134
|
| `@ecsplico/editor/collab`, `/collab/vue`, `/collab/react` | Framework-free: `collabFeatureCore(ui?)` (Yjs `Collaboration`/`CollaborationCaret`, active only with a `collaboration` session on the surface), `yjsAnchorResolver`; `/vue` and `/react`: `collabFeature()` (the Vue one adds the `@tiptap/vue-3` state-sync workaround, the React one needs none) |
|
|
135
|
-
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | `/vue` and `/react`: `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay`; framework-free: `blockUiFeatureCore`, `blockActionsFor`, `effectiveTarget
|
|
135
|
+
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | `/vue` and `/react`: `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay`; framework-free: `blockUiFeatureCore`, `blockActionsFor`, `effectiveTarget`, `BLOCK_CORNER_ATTR` and `blockCornerOf` (a node view marks its own top-right icons with `data-block-corner`; the hover bar docks left of them): drag handle, hover targeting and block actions (opt-in; the simple editor never includes it) |
|
|
136
136
|
| `@ecsplico/editor/schema/advanced` | Vue-free `advancedSchemaExtensions()`: the advanced schema for servers, validators and migrations |
|
|
137
137
|
| `@ecsplico/editor/server`, `/server/handlers`, `/server/node`, `/nuxt` | Server side without a web framework: `createCollabServer`, `createAiModels`, `createRateLimiter`, `EditorAdapter`, one request handler per route (`/server/handlers`), `listenCollab` for a plain Node script (`/server/node`); the Nitro registry `setEditorAdapter`, `forgetCollabDocument`, `liveContent`, `collabStats`; the Nuxt module (config key `tiptapEditor`) wraps the same core. See the [integration guide](./docs/integration.md) |
|
|
138
138
|
| `@ecsplico/editor/styles/core.css` (required), `/composites.css`, `/media.css`, `/mentions.css`, `/tables.css`, `/authors.css`, `/comments.css`, `/transcript.css`, `/ai.css`, `/collab.css`, `/block-ui.css`, `/themes.css` | Feature CSS (import after Tailwind, Nuxt UI (Vue) or `ui-vars.css` (React) and `core.css`; `themes.css` after the feature sheets). The Nuxt module adds all of them itself |
|
|
@@ -12,16 +12,16 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
12
12
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
13
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
14
|
"update:modelValue": (text: string) => any;
|
|
15
|
-
save: (text: string) => any;
|
|
16
15
|
ready: (view: EditorView) => any;
|
|
16
|
+
save: (text: string) => any;
|
|
17
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
18
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
19
|
-
onSave?: ((text: string) => any) | undefined;
|
|
20
19
|
onReady?: ((view: EditorView) => any) | undefined;
|
|
20
|
+
onSave?: ((text: string) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
22
|
editable: boolean;
|
|
23
|
-
wordWrap: boolean;
|
|
24
23
|
modelValue: string;
|
|
24
|
+
wordWrap: boolean;
|
|
25
25
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: typeof __VLS_export;
|
|
27
27
|
//#endregion
|
|
@@ -33,16 +33,16 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
|
|
|
33
33
|
};
|
|
34
34
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
35
35
|
"update:modelValue": (text: string) => any;
|
|
36
|
-
save: (text: string) => any;
|
|
37
36
|
ready: (target: DocumentSurfaceTarget) => any;
|
|
37
|
+
save: (text: string) => any;
|
|
38
38
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
39
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
40
|
-
onSave?: ((text: string) => any) | undefined;
|
|
41
40
|
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
41
|
+
onSave?: ((text: string) => any) | undefined;
|
|
42
42
|
}>, {
|
|
43
43
|
editable: boolean;
|
|
44
|
-
wordWrap: boolean;
|
|
45
44
|
modelValue: string;
|
|
45
|
+
wordWrap: boolean;
|
|
46
46
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
47
47
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
48
48
|
declare const _default: typeof __VLS_export;
|
|
@@ -13,5 +13,26 @@ export interface BlockActionEntry {
|
|
|
13
13
|
export declare function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget;
|
|
14
14
|
/** The actions `target` offers; a `menu` action only when its menu has items. */
|
|
15
15
|
export declare function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[]): BlockActionEntry[];
|
|
16
|
+
/**
|
|
17
|
+
* Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu
|
|
18
|
+
* buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row
|
|
19
|
+
* instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BLOCK_CORNER_ATTR = "data-block-corner";
|
|
22
|
+
/** Where a block's corner elements sit, in viewport coordinates. */
|
|
23
|
+
export interface BlockCorner {
|
|
24
|
+
/** Left edge of the leftmost corner element. */
|
|
25
|
+
left: number;
|
|
26
|
+
/** Vertical centre of the first corner element. */
|
|
27
|
+
centerY: number;
|
|
28
|
+
/** The marked elements, for a host that wants to watch them for size changes. */
|
|
29
|
+
elements: HTMLElement[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The corner elements `dom` (a block's node view root) marks with `BLOCK_CORNER_ATTR`, or null when none is shown.
|
|
33
|
+
* Elements inside a nested block's own node view belong to that block and do not count; neither do hidden ones (no
|
|
34
|
+
* layout box).
|
|
35
|
+
*/
|
|
36
|
+
export declare function blockCornerOf(dom: HTMLElement): BlockCorner | null;
|
|
16
37
|
//#endregion
|
|
17
38
|
//# sourceMappingURL=block-actions.d.ts.map
|
|
@@ -31,7 +31,31 @@ function blockActionsFor(editor, target, actions) {
|
|
|
31
31
|
}] : [];
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu
|
|
36
|
+
* buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row
|
|
37
|
+
* instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.
|
|
38
|
+
*/
|
|
39
|
+
const BLOCK_CORNER_ATTR = "data-block-corner";
|
|
40
|
+
/**
|
|
41
|
+
* The corner elements `dom` (a block's node view root) marks with `BLOCK_CORNER_ATTR`, or null when none is shown.
|
|
42
|
+
* Elements inside a nested block's own node view belong to that block and do not count; neither do hidden ones (no
|
|
43
|
+
* layout box).
|
|
44
|
+
*/
|
|
45
|
+
function blockCornerOf(dom) {
|
|
46
|
+
const elements = [...dom.querySelectorAll(`[${BLOCK_CORNER_ATTR}]`)].filter((element) => {
|
|
47
|
+
const owner = element.closest("[data-node-view-wrapper]");
|
|
48
|
+
return !(owner && owner !== dom && dom.contains(owner));
|
|
49
|
+
});
|
|
50
|
+
const rects = elements.map((element) => element.getBoundingClientRect()).filter((rect) => rect.width > 0 && rect.height > 0);
|
|
51
|
+
if (!rects.length) return null;
|
|
52
|
+
return {
|
|
53
|
+
left: Math.min(...rects.map((rect) => rect.left)),
|
|
54
|
+
centerY: rects[0].top + rects[0].height / 2,
|
|
55
|
+
elements
|
|
56
|
+
};
|
|
57
|
+
}
|
|
34
58
|
//#endregion
|
|
35
|
-
export { blockActionsFor, effectiveTarget };
|
|
59
|
+
export { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, effectiveTarget };
|
|
36
60
|
|
|
37
61
|
//# sourceMappingURL=block-actions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-actions.js","names":[],"sources":["../../../src/features/block-ui/block-actions.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { abilitiesAt, type BlockAction, type BlockActionContext, type BlockTarget, type MenuItem } from '../../core'\nimport { isChromeNode } from '../../core/schema'\n\n/** An action `target` offers, with the context it runs with and, for a `menu` action, its items. */\nexport interface BlockActionEntry {\n action: BlockAction\n ctx: BlockActionContext\n menu?: MenuItem[]\n}\n\n/** Hovering a composite's title/description/tags row addresses the composite itself. */\nexport function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget {\n if (!isChromeNode(target.node)) return target\n const $pos = editor.state.doc.resolve(target.pos)\n return { node: $pos.parent, pos: $pos.before() }\n}\n\n/** The actions `target` offers; a `menu` action only when its menu has items. */\nexport function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[]): BlockActionEntry[] {\n const ctx: BlockActionContext = { ...target, editor, abilities: abilitiesAt(editor, target.pos, target.node) }\n return actions.flatMap((action): BlockActionEntry[] => {\n if (!ctx.abilities.has(action.ability) || action.isVisible?.(ctx) === false) return []\n if (!action.menu) return [{ action, ctx }]\n const menu = action.menu(ctx)\n return menu.length ? [{ action, ctx, menu }] : []\n })\n}\n"],"mappings":";;;;AAYA,SAAgB,gBAAgB,QAAgB,QAAkC;CAChF,IAAI,CAAC,aAAa,OAAO,IAAI,GAAG,OAAO;CACvC,MAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,OAAO,GAAG;CAChD,OAAO;EAAE,MAAM,KAAK;EAAQ,KAAK,KAAK,OAAO;CAAE;AACjD;;AAGA,SAAgB,gBAAgB,QAAgB,QAAqB,SAAqD;CACxH,MAAM,MAA0B;EAAE,GAAG;EAAQ;EAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,OAAO,IAAI;CAAE;CAC7G,OAAO,QAAQ,SAAS,WAA+B;EACrD,IAAI,CAAC,IAAI,UAAU,IAAI,OAAO,OAAO,KAAK,OAAO,YAAY,GAAG,MAAM,OAAO,OAAO,CAAC;EACrF,IAAI,CAAC,OAAO,MAAM,OAAO,CAAC;GAAE;GAAQ;EAAI,CAAC;EACzC,MAAM,OAAO,OAAO,KAAK,GAAG;EAC5B,OAAO,KAAK,SAAS,CAAC;GAAE;GAAQ;GAAK;EAAK,CAAC,IAAI,CAAC;CAClD,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"block-actions.js","names":[],"sources":["../../../src/features/block-ui/block-actions.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { abilitiesAt, type BlockAction, type BlockActionContext, type BlockTarget, type MenuItem } from '../../core'\nimport { isChromeNode } from '../../core/schema'\n\n/** An action `target` offers, with the context it runs with and, for a `menu` action, its items. */\nexport interface BlockActionEntry {\n action: BlockAction\n ctx: BlockActionContext\n menu?: MenuItem[]\n}\n\n/** Hovering a composite's title/description/tags row addresses the composite itself. */\nexport function effectiveTarget(editor: Editor, target: BlockTarget): BlockTarget {\n if (!isChromeNode(target.node)) return target\n const $pos = editor.state.doc.resolve(target.pos)\n return { node: $pos.parent, pos: $pos.before() }\n}\n\n/** The actions `target` offers; a `menu` action only when its menu has items. */\nexport function blockActionsFor(editor: Editor, target: BlockTarget, actions: readonly BlockAction[]): BlockActionEntry[] {\n const ctx: BlockActionContext = { ...target, editor, abilities: abilitiesAt(editor, target.pos, target.node) }\n return actions.flatMap((action): BlockActionEntry[] => {\n if (!ctx.abilities.has(action.ability) || action.isVisible?.(ctx) === false) return []\n if (!action.menu) return [{ action, ctx }]\n const menu = action.menu(ctx)\n return menu.length ? [{ action, ctx, menu }] : []\n })\n}\n\n/**\n * Attribute a block's own chrome sets on the elements it shows in its top-right corner (status badges, kind or menu\n * buttons, ...). The hover bar docks immediately left of them, vertically centred on them, so both read as one row\n * instead of the bar covering the block's icons. A block without such elements gets the bar at the editor's right edge.\n */\nexport const BLOCK_CORNER_ATTR = 'data-block-corner'\n\n/** Where a block's corner elements sit, in viewport coordinates. */\nexport interface BlockCorner {\n /** Left edge of the leftmost corner element. */\n left: number\n /** Vertical centre of the first corner element. */\n centerY: number\n /** The marked elements, for a host that wants to watch them for size changes. */\n elements: HTMLElement[]\n}\n\n/**\n * The corner elements `dom` (a block's node view root) marks with `BLOCK_CORNER_ATTR`, or null when none is shown.\n * Elements inside a nested block's own node view belong to that block and do not count; neither do hidden ones (no\n * layout box).\n */\nexport function blockCornerOf(dom: HTMLElement): BlockCorner | null {\n const elements = [...dom.querySelectorAll<HTMLElement>(`[${BLOCK_CORNER_ATTR}]`)].filter((element) => {\n const owner = element.closest('[data-node-view-wrapper]')\n return !(owner && owner !== dom && dom.contains(owner))\n })\n const rects = elements.map(element => element.getBoundingClientRect()).filter(rect => rect.width > 0 && rect.height > 0)\n if (!rects.length) return null\n return {\n left: Math.min(...rects.map(rect => rect.left)),\n centerY: rects[0]!.top + rects[0]!.height / 2,\n elements,\n }\n}\n"],"mappings":";;;;AAYA,SAAgB,gBAAgB,QAAgB,QAAkC;CAChF,IAAI,CAAC,aAAa,OAAO,IAAI,GAAG,OAAO;CACvC,MAAM,OAAO,OAAO,MAAM,IAAI,QAAQ,OAAO,GAAG;CAChD,OAAO;EAAE,MAAM,KAAK;EAAQ,KAAK,KAAK,OAAO;CAAE;AACjD;;AAGA,SAAgB,gBAAgB,QAAgB,QAAqB,SAAqD;CACxH,MAAM,MAA0B;EAAE,GAAG;EAAQ;EAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,OAAO,IAAI;CAAE;CAC7G,OAAO,QAAQ,SAAS,WAA+B;EACrD,IAAI,CAAC,IAAI,UAAU,IAAI,OAAO,OAAO,KAAK,OAAO,YAAY,GAAG,MAAM,OAAO,OAAO,CAAC;EACrF,IAAI,CAAC,OAAO,MAAM,OAAO,CAAC;GAAE;GAAQ;EAAI,CAAC;EACzC,MAAM,OAAO,OAAO,KAAK,GAAG;EAC5B,OAAO,KAAK,SAAS,CAAC;GAAE;GAAQ;GAAK;EAAK,CAAC,IAAI,CAAC;CAClD,CAAC;AACH;;;;;;AAOA,MAAa,oBAAoB;;;;;;AAiBjC,SAAgB,cAAc,KAAsC;CAClE,MAAM,WAAW,CAAC,GAAG,IAAI,iBAA8B,IAAI,kBAAkB,EAAE,CAAC,CAAC,CAAC,QAAQ,YAAY;EACpG,MAAM,QAAQ,QAAQ,QAAQ,0BAA0B;EACxD,OAAO,EAAE,SAAS,UAAU,OAAO,IAAI,SAAS,KAAK;CACvD,CAAC;CACD,MAAM,QAAQ,SAAS,KAAI,YAAW,QAAQ,sBAAsB,CAAC,CAAC,CAAC,QAAO,SAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,CAAC;CACvH,IAAI,CAAC,MAAM,QAAQ,OAAO;CAC1B,OAAO;EACL,MAAM,KAAK,IAAI,GAAG,MAAM,KAAI,SAAQ,KAAK,IAAI,CAAC;EAC9C,SAAS,MAAM,EAAE,CAAE,MAAM,MAAM,EAAE,CAAE,SAAS;EAC5C;CACF;AACF"}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { useUiKit } from "../../../vue/kit/use-ui-kit.js";
|
|
2
|
-
import { blockActionsFor, effectiveTarget } from "../block-actions.js";
|
|
2
|
+
import { blockActionsFor, blockCornerOf, effectiveTarget } from "../block-actions.js";
|
|
3
3
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, unref, useTemplateRef, watch, withCtx, withModifiers } from "vue";
|
|
4
4
|
//#region src/features/block-ui/components/BlockActionsOverlay.vue
|
|
5
5
|
/** Gap between the bar and the editor's right edge. */
|
|
6
6
|
const EDGE_GUTTER = 8;
|
|
7
|
+
/** Gap between the bar and the block's own corner elements it docks next to. */
|
|
8
|
+
const CORNER_GAP = 4;
|
|
7
9
|
/**
|
|
8
10
|
* The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and
|
|
9
11
|
* targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
|
|
10
12
|
* chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
|
|
11
13
|
* read-only. `menu` actions open a dropdown of their items instead of running on click.
|
|
14
|
+
*
|
|
15
|
+
* A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
|
|
16
|
+
* the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
|
|
17
|
+
* it sits at the editor's right edge, aligned to the block's top.
|
|
12
18
|
*/
|
|
13
19
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
14
20
|
__name: "BlockActionsOverlay",
|
|
@@ -43,13 +49,33 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
43
49
|
const items = computed(() => block.value ? blockActionsFor(props.editor, block.value, props.actions) : []);
|
|
44
50
|
const top = ref(0);
|
|
45
51
|
const right = ref(0);
|
|
52
|
+
/** Whether the bar is docked next to the block's corner elements: `top` is then their vertical centre. */
|
|
53
|
+
const docked = ref(false);
|
|
46
54
|
const measured = ref(false);
|
|
55
|
+
let watched = [];
|
|
56
|
+
function watchCorner(elements) {
|
|
57
|
+
if (elements.length === watched.length && elements.every((element, index) => element === watched[index])) return;
|
|
58
|
+
for (const element of watched) resizeObserver?.unobserve(element);
|
|
59
|
+
watched = elements;
|
|
60
|
+
for (const element of watched) resizeObserver?.observe(element);
|
|
61
|
+
}
|
|
47
62
|
function measure() {
|
|
48
63
|
const { editor, host } = props;
|
|
49
64
|
const dom = block.value && host && !editor.isDestroyed ? editor.view.nodeDOM(block.value.pos) : null;
|
|
50
65
|
measured.value = dom instanceof HTMLElement;
|
|
51
|
-
if (!(dom instanceof HTMLElement) || !host)
|
|
66
|
+
if (!(dom instanceof HTMLElement) || !host) {
|
|
67
|
+
watchCorner([]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
52
70
|
const hostRect = host.getBoundingClientRect();
|
|
71
|
+
const corner = blockCornerOf(dom);
|
|
72
|
+
watchCorner(corner?.elements ?? []);
|
|
73
|
+
docked.value = corner !== null;
|
|
74
|
+
if (corner) {
|
|
75
|
+
top.value = corner.centerY - hostRect.top;
|
|
76
|
+
right.value = hostRect.right - corner.left + CORNER_GAP;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
53
79
|
const rect = dom.getBoundingClientRect();
|
|
54
80
|
top.value = rect.top - hostRect.top + 2;
|
|
55
81
|
right.value = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER;
|
|
@@ -76,21 +102,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
76
102
|
window.addEventListener("scroll", measure, true);
|
|
77
103
|
resizeObserver = new ResizeObserver(measure);
|
|
78
104
|
resizeObserver.observe(props.editor.view.dom);
|
|
105
|
+
for (const element of watched) resizeObserver.observe(element);
|
|
79
106
|
});
|
|
80
107
|
onBeforeUnmount(() => {
|
|
81
108
|
window.removeEventListener("resize", measure);
|
|
82
109
|
window.removeEventListener("scroll", measure, true);
|
|
83
110
|
resizeObserver?.disconnect();
|
|
111
|
+
watched = [];
|
|
84
112
|
});
|
|
85
113
|
return (_ctx, _cache) => {
|
|
86
114
|
return visible.value ? (openBlock(), createElementBlock("div", {
|
|
87
115
|
key: 0,
|
|
88
116
|
ref_key: "root",
|
|
89
117
|
ref: root,
|
|
90
|
-
class: "block-actions absolute z-10 flex gap-0.5 rounded-md
|
|
118
|
+
class: "block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5",
|
|
91
119
|
style: normalizeStyle({
|
|
92
120
|
top: `${top.value}px`,
|
|
93
|
-
right: `${right.value}px
|
|
121
|
+
right: `${right.value}px`,
|
|
122
|
+
transform: docked.value ? "translateY(-50%)" : void 0
|
|
94
123
|
}),
|
|
95
124
|
onMouseenter: _cache[2] || (_cache[2] = ($event) => emit("hover", true)),
|
|
96
125
|
onMouseleave: onMouseLeave
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockActionsOverlay.js","names":["_openBlock","_createElementBlock","_normalizeStyle","_Fragment","_renderList","_createBlock","_unref","_withCtx","_createVNode","_withModifiers","_createCommentVNode"],"sources":["../../../../src/features/block-ui/components/BlockActionsOverlay.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport type { Editor } from '@tiptap/vue-3'\nimport { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport type { BlockAction, BlockTarget } from '../../../core'\nimport { blockActionsFor, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n */\nconst props = defineProps<{\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n}>()\n\nconst { Button: UButton, DropdownMenu: UDropdownMenu, Tooltip: UTooltip } = useUiKit()\n\nconst emit = defineEmits<{ hover: [hovered: boolean] }>()\n\n/** The target in the current document; null once its position holds something else (deleted, replaced). */\nconst block = computed<BlockTarget | null>(() => {\n const { editor, target } = props\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n})\n\n// Reading `state` makes the flag follow `setEditable` (which dispatches a transaction).\nconst editable = computed(() => {\n void props.editor.state\n return props.editor.isEditable\n})\n\nconst items = computed(() => (block.value ? blockActionsFor(props.editor, block.value, props.actions) : []))\n\nconst top = ref(0)\nconst right = ref(0)\nconst measured = ref(false)\n\nfunction measure() {\n const { editor, host } = props\n const dom = block.value && host && !editor.isDestroyed ? editor.view.nodeDOM(block.value.pos) : null\n measured.value = dom instanceof HTMLElement\n if (!(dom instanceof HTMLElement) || !host) return\n const hostRect = host.getBoundingClientRect()\n const rect = dom.getBoundingClientRect()\n top.value = rect.top - hostRect.top + 2\n right.value = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n}\n\nwatch([block, () => props.editor.state], measure, { flush: 'post' })\n\nconst visible = computed(() => editable.value && items.value.length > 0 && measured.value)\n\n// An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\nconst root = useTemplateRef<HTMLElement>('root')\nconst menuOpen = ref(false)\nfunction onMenuOpen(open: boolean) {\n menuOpen.value = open\n emit('hover', open || !!root.value?.matches(':hover'))\n}\nfunction onMouseLeave() {\n if (!menuOpen.value) emit('hover', false)\n}\n\n// A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\nwatch(visible, (value) => {\n if (value) return\n menuOpen.value = false\n emit('hover', false)\n})\n\n// The bar follows the editor's right edge when the column resizes without the window doing so.\nlet resizeObserver: ResizeObserver | undefined\nonMounted(() => {\n window.addEventListener('resize', measure)\n window.addEventListener('scroll', measure, true)\n resizeObserver = new ResizeObserver(measure)\n resizeObserver.observe(props.editor.view.dom)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', measure)\n window.removeEventListener('scroll', measure, true)\n resizeObserver?.disconnect()\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n ref=\"root\"\n class=\"block-actions absolute z-10 flex gap-0.5 rounded-md border border-[var(--sk-border)] bg-[var(--sk-bg)] p-0.5 shadow-sm\"\n :style=\"{ top: `${top}px`, right: `${right}px` }\"\n @mouseenter=\"emit('hover', true)\"\n @mouseleave=\"onMouseLeave\"\n >\n <template v-for=\"entry in items\" :key=\"entry.action.id\">\n <UDropdownMenu v-if=\"entry.menu\" :items=\"entry.menu\" :content=\"{ align: 'end' }\" @update:open=\"onMenuOpen\">\n <UTooltip :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n />\n </UTooltip>\n </UDropdownMenu>\n <UTooltip v-else :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n @click=\"entry.action.run?.(entry.ctx)\"\n />\n </UTooltip>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BlockActionsOverlay.js","names":["_openBlock","_createElementBlock","_normalizeStyle","_Fragment","_renderList","_createBlock","_unref","_withCtx","_createVNode","_withModifiers","_createCommentVNode"],"sources":["../../../../src/features/block-ui/components/BlockActionsOverlay.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useUiKit } from '../../../vue/kit/use-ui-kit'\nimport type { Editor } from '@tiptap/vue-3'\nimport { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport type { BlockAction, BlockTarget } from '../../../core'\nimport { blockActionsFor, blockCornerOf, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n/** Gap between the bar and the block's own corner elements it docks next to. */\nconst CORNER_GAP = 4\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n *\n * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):\n * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements\n * it sits at the editor's right edge, aligned to the block's top.\n */\nconst props = defineProps<{\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n}>()\n\nconst { Button: UButton, DropdownMenu: UDropdownMenu, Tooltip: UTooltip } = useUiKit()\n\nconst emit = defineEmits<{ hover: [hovered: boolean] }>()\n\n/** The target in the current document; null once its position holds something else (deleted, replaced). */\nconst block = computed<BlockTarget | null>(() => {\n const { editor, target } = props\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n})\n\n// Reading `state` makes the flag follow `setEditable` (which dispatches a transaction).\nconst editable = computed(() => {\n void props.editor.state\n return props.editor.isEditable\n})\n\nconst items = computed(() => (block.value ? blockActionsFor(props.editor, block.value, props.actions) : []))\n\nconst top = ref(0)\nconst right = ref(0)\n/** Whether the bar is docked next to the block's corner elements: `top` is then their vertical centre. */\nconst docked = ref(false)\nconst measured = ref(false)\n\nlet watched: HTMLElement[] = []\n// The corner elements can change size (a badge appears) without the document changing: watch the current ones.\nfunction watchCorner(elements: HTMLElement[]) {\n if (elements.length === watched.length && elements.every((element, index) => element === watched[index])) return\n for (const element of watched) resizeObserver?.unobserve(element)\n watched = elements\n for (const element of watched) resizeObserver?.observe(element)\n}\n\nfunction measure() {\n const { editor, host } = props\n const dom = block.value && host && !editor.isDestroyed ? editor.view.nodeDOM(block.value.pos) : null\n measured.value = dom instanceof HTMLElement\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n docked.value = corner !== null\n if (corner) {\n top.value = corner.centerY - hostRect.top\n right.value = hostRect.right - corner.left + CORNER_GAP\n return\n }\n const rect = dom.getBoundingClientRect()\n top.value = rect.top - hostRect.top + 2\n right.value = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n}\n\nwatch([block, () => props.editor.state], measure, { flush: 'post' })\n\nconst visible = computed(() => editable.value && items.value.length > 0 && measured.value)\n\n// An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\nconst root = useTemplateRef<HTMLElement>('root')\nconst menuOpen = ref(false)\nfunction onMenuOpen(open: boolean) {\n menuOpen.value = open\n emit('hover', open || !!root.value?.matches(':hover'))\n}\nfunction onMouseLeave() {\n if (!menuOpen.value) emit('hover', false)\n}\n\n// A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\nwatch(visible, (value) => {\n if (value) return\n menuOpen.value = false\n emit('hover', false)\n})\n\n// The bar follows the editor's right edge when the column resizes without the window doing so.\nlet resizeObserver: ResizeObserver | undefined\nonMounted(() => {\n window.addEventListener('resize', measure)\n window.addEventListener('scroll', measure, true)\n resizeObserver = new ResizeObserver(measure)\n resizeObserver.observe(props.editor.view.dom)\n for (const element of watched) resizeObserver.observe(element)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', measure)\n window.removeEventListener('scroll', measure, true)\n resizeObserver?.disconnect()\n watched = []\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n ref=\"root\"\n class=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-[var(--sk-bg)] p-0.5\"\n :style=\"{ top: `${top}px`, right: `${right}px`, transform: docked ? 'translateY(-50%)' : undefined }\"\n @mouseenter=\"emit('hover', true)\"\n @mouseleave=\"onMouseLeave\"\n >\n <template v-for=\"entry in items\" :key=\"entry.action.id\">\n <UDropdownMenu v-if=\"entry.menu\" :items=\"entry.menu\" :content=\"{ align: 'end' }\" @update:open=\"onMenuOpen\">\n <UTooltip :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n />\n </UTooltip>\n </UDropdownMenu>\n <UTooltip v-else :text=\"entry.action.label\">\n <UButton\n size=\"xs\"\n color=\"neutral\"\n variant=\"ghost\"\n :icon=\"entry.action.icon\"\n :aria-label=\"entry.action.label\"\n @mousedown.prevent\n @click=\"entry.action.run?.(entry.ctx)\"\n />\n </UTooltip>\n </template>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -7,6 +7,10 @@ import { Editor } from "@tiptap/vue-3";
|
|
|
7
7
|
* targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
|
|
8
8
|
* chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
|
|
9
9
|
* read-only. `menu` actions open a dropdown of their items instead of running on click.
|
|
10
|
+
*
|
|
11
|
+
* A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
|
|
12
|
+
* the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
|
|
13
|
+
* it sits at the editor's right edge, aligned to the block's top.
|
|
10
14
|
*/
|
|
11
15
|
type __VLS_Props = {
|
|
12
16
|
editor: Editor;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BlockActionEntry, blockActionsFor, effectiveTarget } from "./block-actions.js";
|
|
1
|
+
import { BLOCK_CORNER_ATTR, BlockActionEntry, BlockCorner, blockActionsFor, blockCornerOf, effectiveTarget } from "./block-actions.js";
|
|
2
2
|
import { BlockUiUi, blockUiFeatureCore } from "./feature.js";
|
|
3
|
-
export { type BlockActionEntry, type BlockUiUi, blockActionsFor, blockUiFeatureCore, effectiveTarget };
|
|
3
|
+
export { BLOCK_CORNER_ATTR, type BlockActionEntry, type BlockCorner, type BlockUiUi, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { blockUiFeatureCore } from "./feature.js";
|
|
2
|
-
import { blockActionsFor, effectiveTarget } from "./block-actions.js";
|
|
3
|
-
export { blockActionsFor, blockUiFeatureCore, effectiveTarget };
|
|
2
|
+
import { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, effectiveTarget } from "./block-actions.js";
|
|
3
|
+
export { BLOCK_CORNER_ATTR, blockActionsFor, blockCornerOf, blockUiFeatureCore, effectiveTarget };
|
|
@@ -7,6 +7,10 @@ import { Editor } from "@tiptap/core";
|
|
|
7
7
|
* targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
|
|
8
8
|
* chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
|
|
9
9
|
* read-only. `menu` actions open a dropdown of their items instead of running on click.
|
|
10
|
+
*
|
|
11
|
+
* A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
|
|
12
|
+
* the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
|
|
13
|
+
* it sits at the editor's right edge, aligned to the block's top.
|
|
10
14
|
*/
|
|
11
15
|
export declare function BlockActionsOverlay({ editor, target, actions, host, onHover }: {
|
|
12
16
|
editor: Editor;
|
|
@@ -2,17 +2,23 @@ import { Icon } from "../../../react/Icon.js";
|
|
|
2
2
|
import { Button } from "../../../react/ui/button.js";
|
|
3
3
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../react/ui/tooltip.js";
|
|
4
4
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../../react/ui/dropdown-menu.js";
|
|
5
|
-
import { blockActionsFor, effectiveTarget } from "../block-actions.js";
|
|
5
|
+
import { blockActionsFor, blockCornerOf, effectiveTarget } from "../block-actions.js";
|
|
6
6
|
import { useEffect, useLayoutEffect, useReducer, useRef, useState } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/features/block-ui/react/BlockActionsOverlay.tsx
|
|
9
9
|
/** Gap between the bar and the editor's right edge. */
|
|
10
10
|
const EDGE_GUTTER = 8;
|
|
11
|
+
/** Gap between the bar and the block's own corner elements it docks next to. */
|
|
12
|
+
const CORNER_GAP = 4;
|
|
11
13
|
/**
|
|
12
14
|
* The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and
|
|
13
15
|
* targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite
|
|
14
16
|
* chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is
|
|
15
17
|
* read-only. `menu` actions open a dropdown of their items instead of running on click.
|
|
18
|
+
*
|
|
19
|
+
* A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):
|
|
20
|
+
* the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements
|
|
21
|
+
* it sits at the editor's right edge, aligned to the block's top.
|
|
16
22
|
*/
|
|
17
23
|
function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
|
|
18
24
|
const [, rerender] = useReducer((count) => count + 1, 0);
|
|
@@ -35,19 +41,35 @@ function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
|
|
|
35
41
|
})();
|
|
36
42
|
const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : [];
|
|
37
43
|
const [place, setPlace] = useState(null);
|
|
44
|
+
const resizeObserver = useRef(null);
|
|
45
|
+
const watched = useRef([]);
|
|
46
|
+
const watchCorner = (elements) => {
|
|
47
|
+
const current = watched.current;
|
|
48
|
+
if (elements.length === current.length && elements.every((element, index) => element === current[index])) return;
|
|
49
|
+
for (const element of current) resizeObserver.current?.unobserve(element);
|
|
50
|
+
watched.current = elements;
|
|
51
|
+
for (const element of elements) resizeObserver.current?.observe(element);
|
|
52
|
+
};
|
|
38
53
|
const measure = () => {
|
|
39
54
|
const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null;
|
|
40
55
|
if (!(dom instanceof HTMLElement) || !host) {
|
|
56
|
+
watchCorner([]);
|
|
41
57
|
setPlace(null);
|
|
42
58
|
return;
|
|
43
59
|
}
|
|
44
60
|
const hostRect = host.getBoundingClientRect();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
top,
|
|
49
|
-
right
|
|
50
|
-
|
|
61
|
+
const corner = blockCornerOf(dom);
|
|
62
|
+
watchCorner(corner?.elements ?? []);
|
|
63
|
+
const next = corner ? {
|
|
64
|
+
top: corner.centerY - hostRect.top,
|
|
65
|
+
right: hostRect.right - corner.left + CORNER_GAP,
|
|
66
|
+
docked: true
|
|
67
|
+
} : {
|
|
68
|
+
top: dom.getBoundingClientRect().top - hostRect.top + 2,
|
|
69
|
+
right: hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER,
|
|
70
|
+
docked: false
|
|
71
|
+
};
|
|
72
|
+
setPlace((previous) => previous && previous.top === next.top && previous.right === next.right && previous.docked === next.docked ? previous : next);
|
|
51
73
|
};
|
|
52
74
|
useLayoutEffect(measure);
|
|
53
75
|
const visible = editor.isEditable && items.length > 0 && place !== null;
|
|
@@ -57,12 +79,15 @@ function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
|
|
|
57
79
|
const run = () => latestMeasure.current();
|
|
58
80
|
window.addEventListener("resize", run);
|
|
59
81
|
window.addEventListener("scroll", run, true);
|
|
60
|
-
const
|
|
61
|
-
resizeObserver.
|
|
82
|
+
const observer = new ResizeObserver(run);
|
|
83
|
+
resizeObserver.current = observer;
|
|
84
|
+
observer.observe(editor.view.dom);
|
|
85
|
+
for (const element of watched.current) observer.observe(element);
|
|
62
86
|
return () => {
|
|
63
87
|
window.removeEventListener("resize", run);
|
|
64
88
|
window.removeEventListener("scroll", run, true);
|
|
65
|
-
|
|
89
|
+
observer.disconnect();
|
|
90
|
+
resizeObserver.current = null;
|
|
66
91
|
};
|
|
67
92
|
}, [editor]);
|
|
68
93
|
const root = useRef(null);
|
|
@@ -82,10 +107,11 @@ function BlockActionsOverlay({ editor, target, actions, host, onHover }) {
|
|
|
82
107
|
if (!visible || !place) return null;
|
|
83
108
|
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx("div", {
|
|
84
109
|
ref: root,
|
|
85
|
-
className: "block-actions absolute z-10 flex gap-0.5 rounded-md
|
|
110
|
+
className: "block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5",
|
|
86
111
|
style: {
|
|
87
112
|
top: `${place.top}px`,
|
|
88
|
-
right: `${place.right}px
|
|
113
|
+
right: `${place.right}px`,
|
|
114
|
+
transform: place.docked ? "translateY(-50%)" : void 0
|
|
89
115
|
},
|
|
90
116
|
onMouseEnter: () => onHover(true),
|
|
91
117
|
onMouseLeave: () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\nimport { blockActionsFor, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n */\nexport function BlockActionsOverlay({ editor, target, actions, host, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n onHover: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n })()\n const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : []\n\n const [place, setPlace] = useState<{ top: number, right: number } | null>(null)\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const rect = dom.getBoundingClientRect()\n const top = rect.top - hostRect.top + 2\n const right = hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER\n setPlace(previous => (previous && previous.top === top && previous.right === right ? previous : { top, right }))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const resizeObserver = new ResizeObserver(run)\n resizeObserver.observe(editor.view.dom)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n resizeObserver.disconnect()\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n onHover(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n onHover(false)\n }\n wasVisible.current = visible\n }, [visible, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className=\"block-actions absolute z-10 flex gap-0.5 rounded-md border border-border bg-background p-0.5 shadow-sm\"\n style={{ top: `${place.top}px`, right: `${place.right}px` }}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => { if (!menuOpen.current) onHover(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BlockActionsOverlay.js","names":[],"sources":["../../../../src/features/block-ui/react/BlockActionsOverlay.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'\nimport type { BlockAction, BlockTarget, MenuItem } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../react/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../react/ui/tooltip'\nimport { blockActionsFor, blockCornerOf, effectiveTarget } from '../block-actions'\n\n/** Gap between the bar and the editor's right edge. */\nconst EDGE_GUTTER = 8\n/** Gap between the bar and the block's own corner elements it docks next to. */\nconst CORNER_GAP = 4\n\n/**\n * The hover actions of a block, right-aligned at the editor's text column (the drag handle sits on the block's left and\n * targets the same block). Positioned absolutely inside `host`, a `relative` element around the editor; the composite\n * chrome (title, description, tags) addresses the composite (`effectiveTarget`). The bar is hidden while the editor is\n * read-only. `menu` actions open a dropdown of their items instead of running on click.\n *\n * A block that shows icons of its own in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR`):\n * the bar then docks immediately left of them, centred on their row, instead of covering them. Without such elements\n * it sits at the editor's right edge, aligned to the block's top.\n */\nexport function BlockActionsOverlay({ editor, target, actions, host, onHover }: {\n editor: Editor\n target: BlockTarget | null\n actions: readonly BlockAction[]\n host: HTMLElement | null\n onHover: (hovered: boolean) => void\n}) {\n // The document and `isEditable` change with transactions (`setEditable` dispatches one too): render on each.\n const [, rerender] = useReducer((count: number) => count + 1, 0)\n useEffect(() => {\n editor.on('transaction', rerender)\n return () => { editor.off('transaction', rerender) }\n }, [editor])\n\n // The target in the current document; null once its position holds something else (deleted, replaced).\n const block = ((): BlockTarget | null => {\n if (!target || editor.isDestroyed) return null\n const { doc } = editor.state\n if (target.pos < 0 || target.pos >= doc.content.size) return null\n const node = doc.nodeAt(target.pos)\n if (!node || node.type !== target.node.type) return null\n return effectiveTarget(editor, { node, pos: target.pos })\n })()\n const items = block && editor.isEditable ? blockActionsFor(editor, block, actions) : []\n\n // `docked`: the bar sits next to the block's corner elements, and `top` is then their vertical centre.\n const [place, setPlace] = useState<{ top: number, right: number, docked: boolean } | null>(null)\n const resizeObserver = useRef<ResizeObserver | null>(null)\n const watched = useRef<HTMLElement[]>([])\n // The corner elements can change size (a badge appears) without the document changing: watch the current ones.\n const watchCorner = (elements: HTMLElement[]) => {\n const current = watched.current\n if (elements.length === current.length && elements.every((element, index) => element === current[index])) return\n for (const element of current) resizeObserver.current?.unobserve(element)\n watched.current = elements\n for (const element of elements) resizeObserver.current?.observe(element)\n }\n const measure = () => {\n const dom = block && host && !editor.isDestroyed ? editor.view.nodeDOM(block.pos) : null\n if (!(dom instanceof HTMLElement) || !host) {\n watchCorner([])\n setPlace(null)\n return\n }\n const hostRect = host.getBoundingClientRect()\n const corner = blockCornerOf(dom)\n watchCorner(corner?.elements ?? [])\n const next = corner\n ? { top: corner.centerY - hostRect.top, right: hostRect.right - corner.left + CORNER_GAP, docked: true }\n : {\n top: dom.getBoundingClientRect().top - hostRect.top + 2,\n right: hostRect.right - editor.view.dom.getBoundingClientRect().right + EDGE_GUTTER,\n docked: false,\n }\n setPlace(previous => (previous && previous.top === next.top && previous.right === next.right && previous.docked === next.docked ? previous : next))\n }\n // After every render: the block, the document or the layout may have moved it. Equal positions bail out of the state update.\n useLayoutEffect(measure)\n\n const visible = editor.isEditable && items.length > 0 && place !== null\n\n // The bar follows the editor's right edge when the column resizes without the window doing so.\n const latestMeasure = useRef(measure)\n latestMeasure.current = measure\n useEffect(() => {\n const run = () => latestMeasure.current()\n window.addEventListener('resize', run)\n window.addEventListener('scroll', run, true)\n const observer = new ResizeObserver(run)\n resizeObserver.current = observer\n observer.observe(editor.view.dom)\n for (const element of watched.current) observer.observe(element)\n return () => {\n window.removeEventListener('resize', run)\n window.removeEventListener('scroll', run, true)\n observer.disconnect()\n resizeObserver.current = null\n }\n }, [editor])\n\n // An open action menu is rendered outside the overlay (portal): the overlay counts as hovered until the menu closes.\n const root = useRef<HTMLDivElement>(null)\n const menuOpen = useRef(false)\n const onMenuOpenChange = (open: boolean) => {\n menuOpen.current = open\n onHover(open || !!root.current?.matches(':hover'))\n }\n\n // A vanishing overlay sends no mouseleave: report it, so the host does not keep waiting for one.\n const wasVisible = useRef(false)\n useEffect(() => {\n if (wasVisible.current && !visible) {\n menuOpen.current = false\n onHover(false)\n }\n wasVisible.current = visible\n }, [visible, onHover])\n\n if (!visible || !place) return null\n return (\n <TooltipProvider>\n <div\n ref={root}\n className=\"block-actions absolute z-10 flex gap-0.5 rounded-md bg-background p-0.5\"\n style={{ top: `${place.top}px`, right: `${place.right}px`, transform: place.docked ? 'translateY(-50%)' : undefined }}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => { if (!menuOpen.current) onHover(false) }}\n >\n {items.map((entry) => {\n const button = (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={entry.action.label}\n onMouseDown={event => event.preventDefault()}\n onClick={entry.menu ? undefined : () => entry.action.run?.(entry.ctx)}\n >\n <Icon name={entry.action.icon} />\n </Button>\n )\n if (!entry.menu) {\n return (\n <Tooltip key={entry.action.id}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n )\n }\n return (\n <DropdownMenu key={entry.action.id} onOpenChange={onMenuOpenChange}>\n <Tooltip>\n <TooltipTrigger asChild>\n <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>\n </TooltipTrigger>\n <TooltipContent>{entry.action.label}</TooltipContent>\n </Tooltip>\n <DropdownMenuContent align=\"end\">\n {entry.menu.map((item, index) => <MenuRow key={index} item={item} />)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction MenuRow({ item }: { item: MenuItem }) {\n if (item.type === 'label') return <DropdownMenuLabel>{item.label}</DropdownMenuLabel>\n if (item.type === 'separator') return <DropdownMenuSeparator />\n return (\n <DropdownMenuItem disabled={item.disabled} onSelect={() => item.onSelect?.()}>\n {item.icon && <Icon name={item.icon} />}\n <span>{item.label}</span>\n {item.description && <span className=\"ml-auto text-xs text-muted-foreground\">{item.description}</span>}\n </DropdownMenuItem>\n )\n}\n"],"mappings":"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"}
|
|
@@ -40,22 +40,22 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
40
40
|
settingsOpen: import("vue").WritableComputedRef<boolean, boolean>;
|
|
41
41
|
settingsTab: import("vue").WritableComputedRef<string | undefined, string | undefined>;
|
|
42
42
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
43
|
-
"update:modelValue": (markdown: string) => any;
|
|
44
|
-
ready: (editor: Editor) => any;
|
|
45
43
|
settingsOpen: (tab?: string | undefined) => any;
|
|
46
44
|
"update:sidebarOpen": (value: boolean) => any;
|
|
47
45
|
"update:commentsOpen": (value: boolean) => any;
|
|
46
|
+
"update:modelValue": (markdown: string) => any;
|
|
48
47
|
frontmatter: (frontmatter: string | null) => any;
|
|
48
|
+
ready: (editor: Editor) => any;
|
|
49
49
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
50
50
|
contentError: (error: Error) => any;
|
|
51
51
|
diagnostics: (diagnostics: MarkdownDiagnostic[]) => any;
|
|
52
52
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
53
|
-
"onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
|
|
54
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
55
53
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
56
54
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
57
55
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
56
|
+
"onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
|
|
58
57
|
onFrontmatter?: ((frontmatter: string | null) => any) | undefined;
|
|
58
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
59
59
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
60
60
|
onContentError?: ((error: Error) => any) | undefined;
|
|
61
61
|
onDiagnostics?: ((diagnostics: MarkdownDiagnostic[]) => any) | undefined;
|
package/dist/tree/tree-host.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { findBlockById } from "../core/schema/block-ids.js";
|
|
2
2
|
import { compositeJSON, compositeKindsOf } from "../features/composites/nodes.js";
|
|
3
|
-
import { revealBlockInEditor } from "../outline/heading-outline.js";
|
|
4
3
|
import { moveBlockInto, moveIntoVerdict } from "../features/composites/outline.js";
|
|
5
4
|
import { pickFiles } from "../features/media/upload.js";
|
|
5
|
+
import { revealBlockInEditor } from "../outline/heading-outline.js";
|
|
6
6
|
import { readOutline } from "./tree-items.js";
|
|
7
7
|
//#region src/tree/tree-host.ts
|
|
8
8
|
/** The model's view of `editor`: its Baustein kinds, outline and move verdicts (worded in the language of `t`), read live. Create it once per editor. */
|
package/dist/tree/tree-items.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compositeKind, defaultCompositeKinds, kindAccepts } from "../features/composites/kinds.js";
|
|
2
|
-
import { headingOutline } from "../outline/heading-outline.js";
|
|
3
2
|
import { compositeOutline } from "../features/composites/outline.js";
|
|
3
|
+
import { headingOutline } from "../outline/heading-outline.js";
|
|
4
4
|
import { folderKey } from "./folder-paths.js";
|
|
5
5
|
//#region src/tree/tree-items.ts
|
|
6
6
|
/** Key of the virtual-mode root item (the open document). */
|
|
@@ -46,19 +46,19 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
|
46
46
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
47
47
|
editor: import("vue").ComputedRef<Editor | undefined>;
|
|
48
48
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
49
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
50
|
-
ready: (editor: Editor) => any;
|
|
51
49
|
settingsOpen: (tab?: string | undefined) => any;
|
|
52
50
|
"update:sidebarOpen": (value: boolean) => any;
|
|
53
51
|
"update:commentsOpen": (value: boolean) => any;
|
|
52
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
53
|
+
ready: (editor: Editor) => any;
|
|
54
54
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
55
55
|
contentError: (error: Error) => any;
|
|
56
56
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
57
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
58
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
59
57
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
60
58
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
61
59
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
60
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
61
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
62
62
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
63
63
|
onContentError?: ((error: Error) => any) | undefined;
|
|
64
64
|
}>, {
|
|
@@ -43,19 +43,19 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
43
43
|
settingsOpen: import("vue").Ref<boolean, boolean>;
|
|
44
44
|
settingsTab: import("vue").Ref<string | undefined, string | undefined>;
|
|
45
45
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
46
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
47
|
-
ready: (editor: Editor) => any;
|
|
48
46
|
settingsOpen: (tab?: string | undefined) => any;
|
|
49
47
|
"update:sidebarOpen": (value: boolean) => any;
|
|
50
48
|
"update:commentsOpen": (value: boolean) => any;
|
|
49
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
50
|
+
ready: (editor: Editor) => any;
|
|
51
51
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
52
52
|
contentError: (error: Error) => any;
|
|
53
53
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
54
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
55
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
56
54
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
57
55
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
58
56
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
57
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
58
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
59
59
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
60
60
|
onContentError?: ((error: Error) => any) | undefined;
|
|
61
61
|
}>, {
|
package/docs/integration.md
CHANGED
|
@@ -28,7 +28,7 @@ How a host composes the editor from features, where settings and view state live
|
|
|
28
28
|
| `@ecsplico/editor/server/handlers` | server | One handler per editor/AI route, `(ctx, request) => Promise<HandlerResponse>` (`usersGet`, `tagsGet`, `settingsGet/Put`, `assetGet/Post`, `themesGet`, `themeGet/Post/Delete`, `modelsGet`, `chatPost`, `generatePost`, `completePost`, `transcribePost`, `transcriptCleanupPost`) and the zod schemas of their bodies (`chatBodySchema`, …). Map any HTTP server's request onto `HandlerRequest` and send the `HandlerResponse`; `HttpError` carries the status. |
|
|
29
29
|
| `@ecsplico/editor/server/node` | server (Node) | `listenCollab(collab, { port, host })`: a Node HTTP server that serves a `createCollabServer` over websockets, for a plain script (needs the `crossws` dependency; Node only). |
|
|
30
30
|
| `@ecsplico/editor/collab`, `/collab/vue`, `/collab/react` | browser | framework-free `collabFeatureCore(ui?)`, `yjsAnchorResolver`; `collabFeature()` per surface |
|
|
31
|
-
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | browser | framework-free `blockUiFeatureCore(ui)`, `blockActionsFor`, `effectiveTarget`; `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay` per surface |
|
|
31
|
+
| `@ecsplico/editor/block-ui`, `/block-ui/vue`, `/block-ui/react` | browser | framework-free `blockUiFeatureCore(ui)`, `blockActionsFor`, `effectiveTarget`, `BLOCK_CORNER_ATTR`, `blockCornerOf`; `blockUiFeature()`, `BlockUi`, `BlockActionsOverlay` per surface |
|
|
32
32
|
| `@ecsplico/editor/authors`, `/authors/vue`, `/authors/react` | browser | framework-free `authorsFeatureCore(ui)`, `currentAuthor`, `documentAuthors`; `authorsFeature()` plus `AuthorColorsButton`, `AuthorColorsPanel`, `SettingsAuthorsTab` per surface (see the [usage guide](./usage.md#authors-and-comments)) |
|
|
33
33
|
| `@ecsplico/editor/comments`, `/comments/vue`, `/comments/react` | browser | framework-free `commentsFeatureCore(ui, { model? })`, `createCommentsModel`; `commentsFeature({ model? })` (requires `collab` unless a host `model` backs it), `useComments`, `CommentsMargin`, `CommentsPopover`, `CommentsListMenu`, … per surface |
|
|
34
34
|
| `@ecsplico/editor/themes`, `/themes/vue`, `/themes/react` | browser | framework-free `themesFeatureCore(ui)`, `startObsidianTheme`, `scopeThemeCss`, `THEME_SCOPE_CLASS`; `themesFeature()` (site-wide Obsidian themes; see the [usage guide](./usage.md#themes-obsidian)) and `SettingsAppearanceTab` per surface; Vue-free in `/themes/schema`: `ThemeRecord`, `THEME_ID`, `MAX_THEME_BYTES`, `themeManifestSchema` |
|
|
@@ -361,6 +361,8 @@ Which blocks have which handle follows the settings and the block:
|
|
|
361
361
|
- **`blocks.dragHandle: 'hover'`** (default): one handle follows the hovered block.
|
|
362
362
|
- **`blocks.dragHandle: 'always'`**: the hover handle is replaced by one permanent handle per eligible block, each at the position the hover handle would take for that block (right edge on the block's left edge, centred on its first text line; list items left of their bullet). Eligible are the blocks the hover handle can target at any depth: paragraphs, headings, lists' items, quotes, code blocks, media, tables and the blocks inside quotes, list items and table cells. Not eligible: composite chrome (title, description, tags), table rows and cells themselves, list wrappers, the first block of a list item (the item has the handle), and blocks with a kind icon. A nested block whose handle would cover its container's handle on the same line (the first block of a quote) has none; the container's handle moves it. Only blocks within one screen of the viewport render a handle. Both modes are hidden while the editor is read-only.
|
|
363
363
|
|
|
364
|
+
**Blocks with icons of their own in the top-right corner.** The hover bar sits at the editor's right edge, which a node view that draws its own icons there (status badges, a kind or properties button) would have covered. Such a block marks those elements with the attribute `data-block-corner` (`BLOCK_CORNER_ATTR`, `blockCornerOf(dom)` from `@ecsplico/editor/block-ui`): the bar docks immediately left of the leftmost marked element (4px gap) and is centred on the first one's row, so bar and icons read as one row. Marked elements inside a nested block's own node view (`[data-node-view-wrapper]`) belong to that block, hidden ones (no layout box) are ignored, and a block without marked elements keeps the placement at the editor's right edge. The overlay re-measures when a marked element resizes; Vue and React behave the same.
|
|
365
|
+
|
|
364
366
|
`startBlockDrag(editor, event, pos)` (`@ecsplico/editor/core`) starts such a drag from any element outside the editor's DOM: call it from the element's `dragstart`.
|
|
365
367
|
|
|
366
368
|
## 8. Vue-free advanced schema for servers
|
package/docs/llms.md
CHANGED
|
@@ -274,7 +274,7 @@ const preset = defineEditor({ features: [...advancedEditor().features, notesFeat
|
|
|
274
274
|
Things that bite:
|
|
275
275
|
|
|
276
276
|
- Items gated by an own `ability` (`commandItem({ ability })`, `SlashCommandItem.ability`, `BlockAction.ability`) stay inert until the ability exists at the caret. Grant custom abilities at root through the surface prop: `:abilities="{ root: [...ROOT_ABILITIES, MY_ABILITY] }"` (`ROOT_ABILITIES` from `/core/schema`).
|
|
277
|
-
- The hover bar of `BlockAction`s is right-aligned 8px inside the editor's right edge for every block and is hidden while the editor is read-only.
|
|
277
|
+
- The hover bar of `BlockAction`s is right-aligned 8px inside the editor's right edge for every block and is hidden while the editor is read-only. A block that draws its own icons in its top-right corner marks them with `data-block-corner` (`BLOCK_CORNER_ATTR` from `/block-ui`): the bar then docks immediately left of them, centred on their row, instead of covering them.
|
|
278
278
|
- A feature that adds a **node** must be known to every schema that touches the document. The Nuxt module's collab route builds its schema from `advancedSchemaExtensions()` plus the adapter's `schemaExtensions()`, so register the Vue-free node file there (`schemaExtensions: () => [YourNode]`, [extending guide, Collaboration and the server schema](./extending.md#collaboration-and-the-server-schema)); a custom Hocuspocus server builds `getSchema([...advancedSchemaExtensions(), YourNode])` itself. Standalone editors (JSON `v-model`) need none of this. `editorBlock: true` (via `NodeFlags`) gives an own node block IDs, also on the server.
|
|
279
279
|
- A tab component can bind only to fields that exist in `EditorSettings`: `parseEditorSettings` drops unknown keys, so new keys are neither stored locally nor sent to the server. The tab component runs inside the app, so it can call `useEditorSettings()` (from `/vue`: `{ settings, ready, synced }` as refs; from `/react`: the same names as values).
|
|
280
280
|
- Factories (`extensions`, `toolbar`, `slashCommands`, ...) receive the `FeatureContext`: `app`, `settings`, `metadata`, `ui`, `notify`, `fetch`, `ai`, `documentId`, `upload`, `collaboration`, `getEditor()`. `settings` is a framework-free store: read `ctx.settings.get()`, change it with `ctx.settings.update(s => { ... })` (or `set`), observe it with `ctx.settings.subscribe(listener)`. `ctx.notify({ id?, title, description?, kind?, icon?, duration? })` shows a user-facing notice (Nuxt UI toast or sonner toast, per surface; features never import a toast library). `ctx.app` is the app context that app-level models key on. Toolbar and block-action menus return core `MenuItem[]` (`type?: 'label' | 'separator'`, `label`, `description`, `icon`, `disabled`, `onSelect`). `getEditor()` is `undefined` while extensions are constructed; use it in event handlers only. Components in `overlays` and `component` toolbar items receive `{ editor }` as props.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecsplico/editor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vue and React editor library on TipTap v3 (Vue: Nuxt UI; React: shadcn/ui): framework-free features with Vue and React bindings, simple and advanced presets, tree-shakeable entries. Built ESM with type declarations.",
|
|
6
6
|
"license": "MIT",
|