@pilotiq/tiptap 3.12.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/Block.d.ts +8 -2
- package/dist/Block.js +9 -3
- package/dist/RichTextField.d.ts +17 -2
- package/dist/RichTextField.js +38 -7
- package/dist/blocks/alert.d.ts +10 -0
- package/dist/blocks/alert.js +23 -0
- package/dist/blocks/faq.d.ts +9 -0
- package/dist/blocks/faq.js +21 -0
- package/dist/blocks/index.d.ts +22 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/keyTakeaways.d.ts +10 -0
- package/dist/blocks/keyTakeaways.js +15 -0
- package/dist/blocks/prosCons.d.ts +9 -0
- package/dist/blocks/prosCons.js +15 -0
- package/dist/blocks/summary.d.ts +9 -0
- package/dist/blocks/summary.js +14 -0
- package/dist/extensions/DragHandleExtension.js +20 -3
- package/dist/extensions/SlashCommandExtension.js +66 -0
- package/dist/extensions/alertVariants.d.ts +44 -0
- package/dist/extensions/alertVariants.js +132 -0
- package/dist/extensions/contentBlocks.d.ts +34 -0
- package/dist/extensions/contentBlocks.js +491 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/AlertNodeView.d.ts +4 -0
- package/dist/react/AlertNodeView.js +92 -0
- package/dist/react/MarkdownEditor.js +19 -0
- package/dist/react/TiptapEditor.js +4 -0
- package/dist/render.d.ts +5 -2
- package/dist/render.js +164 -2
- package/package.json +4 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert variant primitives — shared by the node (`contentBlocks.ts`), its
|
|
3
|
+
* React NodeView (`AlertNodeView.tsx`), and the read-side serializer
|
|
4
|
+
* (`render.ts`). Kept dependency-free (no `@tiptap/*`) so the server
|
|
5
|
+
* renderer can import it without pulling a Tiptap runtime.
|
|
6
|
+
*
|
|
7
|
+
* `info` / `warning` / `success` / `tip` are presets (default icon + accent);
|
|
8
|
+
* `custom` is the user-driven variant (own icon + color, Phase 2).
|
|
9
|
+
*/
|
|
10
|
+
export const ALERT_VARIANTS = ['info', 'warning', 'success', 'tip', 'custom'];
|
|
11
|
+
export const ALERT_VARIANT_LABEL = {
|
|
12
|
+
info: 'Info', warning: 'Warning', success: 'Success', tip: 'Tip', custom: 'Custom',
|
|
13
|
+
};
|
|
14
|
+
/** Coerce an unknown value to a known variant, defaulting to `info`. */
|
|
15
|
+
export function coerceAlertType(value) {
|
|
16
|
+
return ALERT_VARIANTS.includes(String(value)) ? value : 'info';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Curated icon library — inner SVG markup (lucide paths) keyed by name. Shared
|
|
20
|
+
* by the NodeView picker (`AlertNodeView`) and the read-side renderer
|
|
21
|
+
* (`render.ts`) so the editor and published output never drift. Hand-picked
|
|
22
|
+
* (no `lucide-react` dep, no icon font) so the whole set is ~1-2KB gzipped.
|
|
23
|
+
* Wrap in `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
24
|
+
* stroke-width="2" stroke-linecap="round" stroke-linejoin="round">…</svg>`.
|
|
25
|
+
*/
|
|
26
|
+
export const ALERT_ICONS = {
|
|
27
|
+
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
|
28
|
+
warning: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
29
|
+
check: '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
|
|
30
|
+
lightbulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
|
|
31
|
+
sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/>',
|
|
32
|
+
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
|
|
33
|
+
megaphone: '<path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/>',
|
|
34
|
+
flame: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
|
|
35
|
+
star: '<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>',
|
|
36
|
+
heart: '<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>',
|
|
37
|
+
shield: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
|
38
|
+
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
|
|
39
|
+
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/>',
|
|
40
|
+
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>',
|
|
41
|
+
rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
|
|
42
|
+
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
|
43
|
+
error: '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
|
|
44
|
+
pin: '<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
|
|
45
|
+
};
|
|
46
|
+
/** Icon keys, in picker display order. */
|
|
47
|
+
export const ALERT_ICON_KEYS = Object.keys(ALERT_ICONS);
|
|
48
|
+
/** The default icon key per variant (used when the block sets no `icon`). */
|
|
49
|
+
export const VARIANT_DEFAULT_ICON = {
|
|
50
|
+
info: 'info', warning: 'warning', success: 'check', tip: 'lightbulb', custom: 'sparkles',
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the inner SVG for a block: the explicit `icon` override when set +
|
|
54
|
+
* known, else the variant's default. Shared by the editor + read-side render.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveAlertIconInner(iconKey, variant) {
|
|
57
|
+
if (iconKey && ALERT_ICONS[iconKey])
|
|
58
|
+
return ALERT_ICONS[iconKey];
|
|
59
|
+
return ALERT_ICONS[VARIANT_DEFAULT_ICON[variant]];
|
|
60
|
+
}
|
|
61
|
+
// ── Custom SVG (user-pasted) — sanitized allowlist ───────────────────────────
|
|
62
|
+
//
|
|
63
|
+
// Custom SVG is raw user markup that renders on PUBLIC pages, so it's an XSS
|
|
64
|
+
// surface. We sanitize with a conservative pure-string allowlist (no DOMPurify
|
|
65
|
+
// — keeps the bundle lean and works server-side in render.ts): only known
|
|
66
|
+
// presentational SVG tags + geometry/paint attributes survive; scripts, event
|
|
67
|
+
// handlers, external refs (`use`/`image`/`a`/`href`), `<style>`, `<animate>`,
|
|
68
|
+
// `<foreignObject>` and unknown attributes are dropped. Applied on input AND on
|
|
69
|
+
// every render (defence in depth — a tampered stored attr stays safe).
|
|
70
|
+
const SVG_ALLOWED_TAGS = new Set([
|
|
71
|
+
'svg', 'g', 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline', 'polygon',
|
|
72
|
+
'defs', 'lineargradient', 'radialgradient', 'stop', 'title', 'desc', 'clippath',
|
|
73
|
+
]);
|
|
74
|
+
const SVG_ALLOWED_ATTRS = new Set([
|
|
75
|
+
'viewbox', 'xmlns', 'width', 'height', 'preserveaspectratio',
|
|
76
|
+
'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
|
|
77
|
+
'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity', 'stroke-miterlimit',
|
|
78
|
+
'fill-opacity', 'fill-rule', 'clip-rule', 'clip-path', 'opacity', 'transform',
|
|
79
|
+
'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'x1', 'y1', 'x2', 'y2',
|
|
80
|
+
'points', 'class', 'gradientunits', 'gradienttransform', 'offset',
|
|
81
|
+
'stop-color', 'stop-opacity', 'id',
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* Sanitize a user-pasted SVG string to a safe subset. Returns '' for anything
|
|
85
|
+
* that isn't an `<svg>` element or that sanitizes to nothing.
|
|
86
|
+
*/
|
|
87
|
+
export function sanitizeIconSvg(raw) {
|
|
88
|
+
if (typeof raw !== 'string')
|
|
89
|
+
return '';
|
|
90
|
+
let s = raw.trim();
|
|
91
|
+
if (!/^<svg[\s>]/i.test(s))
|
|
92
|
+
return '';
|
|
93
|
+
// Drop comments / CDATA / PIs / doctype, then dangerous element blocks whole.
|
|
94
|
+
s = s.replace(/<!--[\s\S]*?-->/g, '').replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '')
|
|
95
|
+
.replace(/<\?[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[^>]*>/gi, '');
|
|
96
|
+
s = s.replace(/<(script|style|foreignObject)\b[\s\S]*?<\/\1\s*>/gi, '');
|
|
97
|
+
// Allowlist every remaining tag + its attributes.
|
|
98
|
+
s = s.replace(/<\/?([a-zA-Z][\w:-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)\s*(\/?)>/g, (_m, tag, attrs, slash) => {
|
|
99
|
+
const name = String(tag).toLowerCase();
|
|
100
|
+
if (!SVG_ALLOWED_TAGS.has(name))
|
|
101
|
+
return '';
|
|
102
|
+
if (_m.startsWith('</'))
|
|
103
|
+
return `</${name}>`;
|
|
104
|
+
const kept = [];
|
|
105
|
+
const attrRe = /([a-zA-Z_:][\w:.-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/g;
|
|
106
|
+
let am;
|
|
107
|
+
while ((am = attrRe.exec(attrs)) !== null) {
|
|
108
|
+
const an = (am[1] ?? '').toLowerCase();
|
|
109
|
+
if (!SVG_ALLOWED_ATTRS.has(an))
|
|
110
|
+
continue;
|
|
111
|
+
const av = (am[2] ?? '').replace(/^["']|["']$/g, '');
|
|
112
|
+
if (/javascript:|expression\(|[<>]/i.test(av))
|
|
113
|
+
continue;
|
|
114
|
+
kept.push(`${an}="${av.replace(/"/g, '"')}"`);
|
|
115
|
+
}
|
|
116
|
+
return `<${name}${kept.length ? ' ' + kept.join(' ') : ''}${slash ? ' /' : ''}>`;
|
|
117
|
+
});
|
|
118
|
+
s = s.trim();
|
|
119
|
+
return /^<svg[\s>]/i.test(s) ? s : '';
|
|
120
|
+
}
|
|
121
|
+
const ICON_SVG_WRAP_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
|
|
122
|
+
/**
|
|
123
|
+
* Build the FULL `<svg>` string for a block: a sanitized user `iconSvg` when
|
|
124
|
+
* present, else the (library or variant-default) icon wrapped in the standard
|
|
125
|
+
* stroke svg. Shared by the editor NodeView and the read-side renderer.
|
|
126
|
+
*/
|
|
127
|
+
export function buildAlertIconSvg(iconKey, iconSvg, variant) {
|
|
128
|
+
const custom = sanitizeIconSvg(iconSvg);
|
|
129
|
+
if (custom)
|
|
130
|
+
return custom;
|
|
131
|
+
return `${ICON_SVG_WRAP_OPEN}${resolveAlertIconInner(iconKey, variant)}</svg>`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Node, Extension } from '@tiptap/core';
|
|
2
|
+
export { ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, type AlertType } from './alertVariants.js';
|
|
3
|
+
export declare const KeyTakeaways: Node<any, any>;
|
|
4
|
+
export declare const Summary: Node<any, any>;
|
|
5
|
+
export declare const Faq: Node<any, any>;
|
|
6
|
+
export declare const FaqItem: Node<any, any>;
|
|
7
|
+
export declare const FaqQuestion: Node<any, any>;
|
|
8
|
+
export declare const FaqAnswer: Node<any, any>;
|
|
9
|
+
type AnyMd = any;
|
|
10
|
+
/** Parse a directive info string (`alert{type=warning}`) into an attrs map. */
|
|
11
|
+
export declare function parseDirectiveAttrs(info: string): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Register the `:::alert{…}` block rule + HTML renderers on a markdown-it
|
|
14
|
+
* instance. Idempotent per instance. Wired as `Alert`'s `markdown.parse.setup`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function setupAlertDirective(md: AnyMd): void;
|
|
17
|
+
/**
|
|
18
|
+
* `tiptap-markdown` serializer for the Alert node → `:::alert{type=…} Title`.
|
|
19
|
+
* The title rides the opening fence line (admonition style); the body markdown
|
|
20
|
+
* sits between the fences.
|
|
21
|
+
*/
|
|
22
|
+
export declare function serializeAlertMarkdown(state: AnyMd, node: AnyMd): void;
|
|
23
|
+
export declare const Alert: Node<any, any>;
|
|
24
|
+
/** The Alert heading — a single editable line, defaults to the variant label. */
|
|
25
|
+
export declare const AlertTitle: Node<any, any>;
|
|
26
|
+
/** The Alert body — the editable description (`block+`). */
|
|
27
|
+
export declare const AlertBody: Node<any, any>;
|
|
28
|
+
export declare const ProsCons: Node<any, any>;
|
|
29
|
+
export declare const ProsColumn: Node<any, any>;
|
|
30
|
+
export declare const ConsColumn: Node<any, any>;
|
|
31
|
+
export declare const ContentBlockKeymap: Extension<any, any>;
|
|
32
|
+
/** All inline content-block extensions — registered in the editor's list. */
|
|
33
|
+
export declare const contentBlockNodes: (Node<any, any> | Extension<any, any>)[];
|
|
34
|
+
//# sourceMappingURL=contentBlocks.d.ts.map
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { Node, Extension, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
3
|
+
import { AlertNodeView } from '../react/AlertNodeView.js';
|
|
4
|
+
import { coerceAlertType } from './alertVariants.js';
|
|
5
|
+
// Re-exported for back-compat — the canonical definitions live in
|
|
6
|
+
// `alertVariants.ts` (shared with the React NodeView + the read-side renderer).
|
|
7
|
+
export { ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType } from './alertVariants.js';
|
|
8
|
+
/** A labelled region whose body is ordinary editable content (`block+`). */
|
|
9
|
+
function labeledBlock(spec) {
|
|
10
|
+
return Node.create({
|
|
11
|
+
name: spec.name,
|
|
12
|
+
group: 'block',
|
|
13
|
+
content: 'block+',
|
|
14
|
+
defining: true,
|
|
15
|
+
parseHTML() {
|
|
16
|
+
return [{ tag: `div[data-type="${spec.name}"]`, contentElement: '.pilotiq-block-body' }];
|
|
17
|
+
},
|
|
18
|
+
renderHTML({ HTMLAttributes }) {
|
|
19
|
+
return [
|
|
20
|
+
'div',
|
|
21
|
+
mergeAttributes(HTMLAttributes, { 'data-type': spec.name, class: spec.cssClass }),
|
|
22
|
+
['div', { class: 'pilotiq-block-label', contenteditable: 'false' }, spec.label],
|
|
23
|
+
['div', { class: 'pilotiq-block-body' }, 0],
|
|
24
|
+
];
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export const KeyTakeaways = labeledBlock({ name: 'keyTakeaways', label: 'Key takeaways', cssClass: 'pilotiq-key-takeaways' });
|
|
29
|
+
export const Summary = labeledBlock({ name: 'summary', label: 'Summary', cssClass: 'pilotiq-summary' });
|
|
30
|
+
// ── FAQ — structured question / answer items ──
|
|
31
|
+
//
|
|
32
|
+
// `faq` > `faqItem+`; each item is a `faqQuestion` (inline text, "Q" marker) +
|
|
33
|
+
// a `faqAnswer` (block+, "A" marker). Authoring: Enter in a question jumps to
|
|
34
|
+
// its answer; Cmd/Ctrl-Enter inside an item adds a new Q&A item below.
|
|
35
|
+
const NEW_FAQ_ITEM = {
|
|
36
|
+
type: 'faqItem',
|
|
37
|
+
content: [{ type: 'faqQuestion' }, { type: 'faqAnswer', content: [{ type: 'paragraph' }] }],
|
|
38
|
+
};
|
|
39
|
+
export const Faq = Node.create({
|
|
40
|
+
name: 'faq',
|
|
41
|
+
group: 'block',
|
|
42
|
+
content: 'faqItem+',
|
|
43
|
+
defining: true,
|
|
44
|
+
parseHTML() {
|
|
45
|
+
return [{ tag: 'div[data-type="faq"]', contentElement: '.pilotiq-block-body' }];
|
|
46
|
+
},
|
|
47
|
+
renderHTML({ HTMLAttributes }) {
|
|
48
|
+
return [
|
|
49
|
+
'div',
|
|
50
|
+
mergeAttributes(HTMLAttributes, { 'data-type': 'faq', class: 'pilotiq-faq' }),
|
|
51
|
+
['div', { class: 'pilotiq-block-label', contenteditable: 'false' }, 'FAQ'],
|
|
52
|
+
['div', { class: 'pilotiq-block-body' }, 0],
|
|
53
|
+
];
|
|
54
|
+
},
|
|
55
|
+
addKeyboardShortcuts() {
|
|
56
|
+
return {
|
|
57
|
+
// Enter drives the whole Q&A flow:
|
|
58
|
+
// • in a question → jump to its answer;
|
|
59
|
+
// • in an answer → finish it and start a NEW Q&A item below (focus its
|
|
60
|
+
// question). Repeatable: question → Enter → answer → Enter → next.
|
|
61
|
+
// (Shift-Enter still inserts a line break within an answer.)
|
|
62
|
+
Enter: ({ editor }) => {
|
|
63
|
+
const { $from, empty } = editor.state.selection;
|
|
64
|
+
if (!empty)
|
|
65
|
+
return false;
|
|
66
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
67
|
+
const name = $from.node(d).type.name;
|
|
68
|
+
if (name === 'faqQuestion') {
|
|
69
|
+
return editor.chain().setTextSelection($from.after(d) + 1).focus().run();
|
|
70
|
+
}
|
|
71
|
+
if (name === 'faqAnswer') {
|
|
72
|
+
const after = $from.after(d - 1); // position just after the faqItem
|
|
73
|
+
return editor.chain().insertContentAt(after, NEW_FAQ_ITEM).setTextSelection(after + 2).focus().run();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
// Cmd/Ctrl-Enter anywhere in an item also adds a new Q&A item below.
|
|
79
|
+
'Mod-Enter': ({ editor }) => {
|
|
80
|
+
const { $from } = editor.state.selection;
|
|
81
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
82
|
+
if ($from.node(d).type.name === 'faqItem') {
|
|
83
|
+
const after = $from.after(d);
|
|
84
|
+
return editor.chain().insertContentAt(after, NEW_FAQ_ITEM).setTextSelection(after + 2).focus().run();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
},
|
|
89
|
+
// Backspace in an EMPTY question removes that whole Q&A item (and the
|
|
90
|
+
// whole FAQ block if it was the only item) — like emptying a list item.
|
|
91
|
+
Backspace: ({ editor }) => {
|
|
92
|
+
const { $from, empty } = editor.state.selection;
|
|
93
|
+
if (!empty)
|
|
94
|
+
return false;
|
|
95
|
+
let qDepth = -1;
|
|
96
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
97
|
+
if ($from.node(d).type.name === 'faqQuestion') {
|
|
98
|
+
qDepth = d;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (qDepth === -1)
|
|
103
|
+
return false;
|
|
104
|
+
if ($from.node(qDepth).content.size > 0)
|
|
105
|
+
return false; // question not empty → normal backspace
|
|
106
|
+
const faqDepth = qDepth - 2;
|
|
107
|
+
const itemDepth = qDepth - 1;
|
|
108
|
+
const faq = $from.node(faqDepth);
|
|
109
|
+
const faqStart = $from.before(faqDepth);
|
|
110
|
+
if (faq.childCount <= 1) {
|
|
111
|
+
// last item → remove the whole FAQ block
|
|
112
|
+
return editor.chain().deleteRange({ from: faqStart, to: faqStart + faq.nodeSize }).focus().run();
|
|
113
|
+
}
|
|
114
|
+
// remove just this Q&A item; drop the cursor into a neighbouring item
|
|
115
|
+
const itemStart = $from.before(itemDepth);
|
|
116
|
+
const itemEnd = itemStart + $from.node(itemDepth).nodeSize;
|
|
117
|
+
const caret = $from.index(faqDepth) === 0 ? faqStart + 3 : itemStart - 1;
|
|
118
|
+
return editor.chain().deleteRange({ from: itemStart, to: itemEnd }).setTextSelection(caret).focus().run();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
export const FaqItem = Node.create({
|
|
124
|
+
name: 'faqItem',
|
|
125
|
+
group: 'faqItem',
|
|
126
|
+
content: 'faqQuestion faqAnswer',
|
|
127
|
+
defining: true,
|
|
128
|
+
parseHTML() {
|
|
129
|
+
return [{ tag: 'div[data-type="faqItem"]' }];
|
|
130
|
+
},
|
|
131
|
+
renderHTML({ HTMLAttributes }) {
|
|
132
|
+
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'faqItem', class: 'pilotiq-faq-item' }), 0];
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
export const FaqQuestion = Node.create({
|
|
136
|
+
name: 'faqQuestion',
|
|
137
|
+
content: 'inline*',
|
|
138
|
+
defining: true,
|
|
139
|
+
parseHTML() {
|
|
140
|
+
return [{ tag: 'div[data-type="faqQuestion"]', contentElement: '.pilotiq-faq-text' }];
|
|
141
|
+
},
|
|
142
|
+
renderHTML({ HTMLAttributes }) {
|
|
143
|
+
return [
|
|
144
|
+
'div',
|
|
145
|
+
mergeAttributes(HTMLAttributes, { 'data-type': 'faqQuestion', class: 'pilotiq-faq-question' }),
|
|
146
|
+
['span', { class: 'pilotiq-faq-marker', contenteditable: 'false' }, 'Q'],
|
|
147
|
+
['span', { class: 'pilotiq-faq-text' }, 0],
|
|
148
|
+
];
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
export const FaqAnswer = Node.create({
|
|
152
|
+
name: 'faqAnswer',
|
|
153
|
+
content: 'block+',
|
|
154
|
+
defining: true,
|
|
155
|
+
parseHTML() {
|
|
156
|
+
return [{ tag: 'div[data-type="faqAnswer"]', contentElement: '.pilotiq-faq-body' }];
|
|
157
|
+
},
|
|
158
|
+
renderHTML({ HTMLAttributes }) {
|
|
159
|
+
return [
|
|
160
|
+
'div',
|
|
161
|
+
mergeAttributes(HTMLAttributes, { 'data-type': 'faqAnswer', class: 'pilotiq-faq-answer' }),
|
|
162
|
+
['span', { class: 'pilotiq-faq-marker', contenteditable: 'false' }, 'A'],
|
|
163
|
+
['div', { class: 'pilotiq-faq-body' }, 0],
|
|
164
|
+
];
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const DIRECTIVE_MARKER = 0x3a; // ':'
|
|
168
|
+
const DIRECTIVE_MIN_MARKERS = 3;
|
|
169
|
+
/** Parse a directive info string (`alert{type=warning}`) into an attrs map. */
|
|
170
|
+
export function parseDirectiveAttrs(info) {
|
|
171
|
+
const out = {};
|
|
172
|
+
const m = /\{([^}]*)\}/.exec(info);
|
|
173
|
+
if (!m || !m[1])
|
|
174
|
+
return out;
|
|
175
|
+
for (const pair of m[1].split(/[\s,]+/)) {
|
|
176
|
+
const eq = pair.indexOf('=');
|
|
177
|
+
if (eq === -1)
|
|
178
|
+
continue;
|
|
179
|
+
out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/** The directive name — the token before the first `{` or whitespace. */
|
|
184
|
+
function directiveName(info) {
|
|
185
|
+
return info.trim().split(/[\s{]/, 1)[0] ?? '';
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The directive title — the free text after `{attrs}` on the opening fence
|
|
189
|
+
* line (Docusaurus-admonition style: `:::alert{type=warning} Heads up`).
|
|
190
|
+
* Empty when omitted.
|
|
191
|
+
*/
|
|
192
|
+
function directiveTitle(info) {
|
|
193
|
+
const close = info.lastIndexOf('}');
|
|
194
|
+
const rest = close === -1 ? info.replace(/^\S+/, '') : info.slice(close + 1);
|
|
195
|
+
return rest.trim();
|
|
196
|
+
}
|
|
197
|
+
/** Minimal HTML escape for text interpolated into the directive's HTML. */
|
|
198
|
+
function escapeDirectiveHtml(s) {
|
|
199
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Register the `:::alert{…}` block rule + HTML renderers on a markdown-it
|
|
203
|
+
* instance. Idempotent per instance. Wired as `Alert`'s `markdown.parse.setup`.
|
|
204
|
+
*/
|
|
205
|
+
export function setupAlertDirective(md) {
|
|
206
|
+
if (md.__pilotiqAlertDirective)
|
|
207
|
+
return;
|
|
208
|
+
md.__pilotiqAlertDirective = true;
|
|
209
|
+
md.block.ruler.before('fence', 'pilotiqAlert', (state, startLine, endLine, silent) => {
|
|
210
|
+
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
211
|
+
let max = state.eMarks[startLine];
|
|
212
|
+
if (state.src.charCodeAt(pos) !== DIRECTIVE_MARKER)
|
|
213
|
+
return false;
|
|
214
|
+
const memStart = pos;
|
|
215
|
+
while (pos <= max && state.src.charCodeAt(pos) === DIRECTIVE_MARKER)
|
|
216
|
+
pos++;
|
|
217
|
+
const markerCount = pos - memStart;
|
|
218
|
+
if (markerCount < DIRECTIVE_MIN_MARKERS)
|
|
219
|
+
return false;
|
|
220
|
+
const info = state.src.slice(pos, max).trim();
|
|
221
|
+
if (directiveName(info) !== 'alert')
|
|
222
|
+
return false;
|
|
223
|
+
if (silent)
|
|
224
|
+
return true;
|
|
225
|
+
// Scan for the closing fence (>= as many markers, nothing but space after).
|
|
226
|
+
let nextLine = startLine;
|
|
227
|
+
let autoClosed = false;
|
|
228
|
+
for (;;) {
|
|
229
|
+
nextLine++;
|
|
230
|
+
if (nextLine >= endLine)
|
|
231
|
+
break;
|
|
232
|
+
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
233
|
+
max = state.eMarks[nextLine];
|
|
234
|
+
if (state.src.charCodeAt(pos) !== DIRECTIVE_MARKER)
|
|
235
|
+
continue;
|
|
236
|
+
if (state.sCount[nextLine] - state.blkIndent >= 4)
|
|
237
|
+
continue;
|
|
238
|
+
let closePos = pos;
|
|
239
|
+
while (closePos <= max && state.src.charCodeAt(closePos) === DIRECTIVE_MARKER)
|
|
240
|
+
closePos++;
|
|
241
|
+
if (closePos - pos < markerCount)
|
|
242
|
+
continue;
|
|
243
|
+
if (state.src.slice(closePos, max).trim() !== '')
|
|
244
|
+
continue;
|
|
245
|
+
autoClosed = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const oldParent = state.parentType;
|
|
249
|
+
const oldLineMax = state.lineMax;
|
|
250
|
+
state.parentType = 'pilotiqAlert';
|
|
251
|
+
state.lineMax = nextLine;
|
|
252
|
+
const attrs = parseDirectiveAttrs(info);
|
|
253
|
+
let token = state.push('pilotiq_alert_open', 'div', 1);
|
|
254
|
+
token.markup = ':'.repeat(markerCount);
|
|
255
|
+
token.block = true;
|
|
256
|
+
token.info = info;
|
|
257
|
+
token.meta = { type: attrs['type'], icon: attrs['icon'], color: attrs['color'], title: directiveTitle(info) };
|
|
258
|
+
token.map = [startLine, nextLine];
|
|
259
|
+
state.md.block.tokenize(state, startLine + 1, nextLine);
|
|
260
|
+
token = state.push('pilotiq_alert_close', 'div', -1);
|
|
261
|
+
token.markup = ':'.repeat(markerCount);
|
|
262
|
+
token.block = true;
|
|
263
|
+
state.parentType = oldParent;
|
|
264
|
+
state.lineMax = oldLineMax;
|
|
265
|
+
state.line = nextLine + (autoClosed ? 1 : 0);
|
|
266
|
+
return true;
|
|
267
|
+
}, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
268
|
+
// Open emits the alert wrapper + the (closed) title div + the OPEN body div;
|
|
269
|
+
// the tokenized content renders into the body; close shuts body + wrapper.
|
|
270
|
+
// The data-types match `Alert`/`AlertTitle`/`AlertBody`'s `parseHTML`, so the
|
|
271
|
+
// HTML-based tiptap-markdown parse path revives the full node structure.
|
|
272
|
+
md.renderer.rules['pilotiq_alert_open'] = (tokens, idx) => {
|
|
273
|
+
const meta = tokens[idx].meta ?? {};
|
|
274
|
+
const type = coerceAlertType(meta.type);
|
|
275
|
+
const title = escapeDirectiveHtml(String(meta.title ?? ''));
|
|
276
|
+
const icon = meta.icon ? ` data-icon="${escapeDirectiveHtml(String(meta.icon))}"` : '';
|
|
277
|
+
const color = meta.color ? ` data-color="${escapeDirectiveHtml(String(meta.color))}"` : '';
|
|
278
|
+
return (`<div data-type="alert" class="pilotiq-alert pilotiq-alert-${type}" data-alert-type="${type}"${icon}${color}>` +
|
|
279
|
+
`<div data-type="alertTitle" class="pilotiq-alert-title">${title}</div>` +
|
|
280
|
+
`<div data-type="alertBody" class="pilotiq-alert-description">\n`);
|
|
281
|
+
};
|
|
282
|
+
md.renderer.rules['pilotiq_alert_close'] = () => '</div></div>\n';
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `tiptap-markdown` serializer for the Alert node → `:::alert{type=…} Title`.
|
|
286
|
+
* The title rides the opening fence line (admonition style); the body markdown
|
|
287
|
+
* sits between the fences.
|
|
288
|
+
*/
|
|
289
|
+
export function serializeAlertMarkdown(state, node) {
|
|
290
|
+
const type = coerceAlertType(node.attrs?.type);
|
|
291
|
+
const icon = String(node.attrs?.icon ?? '').trim();
|
|
292
|
+
const color = String(node.attrs?.color ?? '').trim();
|
|
293
|
+
const title = String(node.firstChild?.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
294
|
+
const body = node.childCount > 1 ? node.child(node.childCount - 1) : null;
|
|
295
|
+
// `icon` / `color` are space-free tokens (icon keys + hex), so they ride the
|
|
296
|
+
// whitespace-split directive attrs cleanly.
|
|
297
|
+
const attrs = [`type=${type}`];
|
|
298
|
+
if (icon)
|
|
299
|
+
attrs.push(`icon=${icon}`);
|
|
300
|
+
if (color)
|
|
301
|
+
attrs.push(`color=${color}`);
|
|
302
|
+
state.write(`:::alert{${attrs.join(' ')}}${title ? ' ' + title : ''}\n`);
|
|
303
|
+
if (body)
|
|
304
|
+
state.renderContent(body);
|
|
305
|
+
state.ensureNewLine();
|
|
306
|
+
state.write(':::');
|
|
307
|
+
state.closeBlock(node);
|
|
308
|
+
}
|
|
309
|
+
// ── Alert — shadcn-style callout: icon + editable title + editable body ───────
|
|
310
|
+
//
|
|
311
|
+
// `alert` wraps two editable children: `alertTitle` (the heading, defaults to
|
|
312
|
+
// the variant label) and `alertBody` (the description). The in-editor chrome —
|
|
313
|
+
// icon, variant picker, theme styling — lives in the React NodeView
|
|
314
|
+
// (`AlertNodeView`); `renderHTML` here is the headless / clipboard / getHTML
|
|
315
|
+
// fallback, and `render.ts` owns the published read-side HTML. Variant lives on
|
|
316
|
+
// the `type` attr (info/warning/success/tip/custom); `icon` / `color` back the
|
|
317
|
+
// Phase-2 per-block pickers.
|
|
318
|
+
export const Alert = Node.create({
|
|
319
|
+
name: 'alert',
|
|
320
|
+
group: 'block',
|
|
321
|
+
content: 'alertTitle alertBody',
|
|
322
|
+
defining: true,
|
|
323
|
+
// Markdown round-trip — `:::alert{type=…} Title` (only consulted when the
|
|
324
|
+
// `tiptap-markdown` extension is present, i.e. the Markdown editor).
|
|
325
|
+
addStorage() {
|
|
326
|
+
return {
|
|
327
|
+
markdown: {
|
|
328
|
+
serialize: serializeAlertMarkdown,
|
|
329
|
+
parse: { setup: setupAlertDirective },
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
addAttributes() {
|
|
334
|
+
return {
|
|
335
|
+
type: {
|
|
336
|
+
default: 'info',
|
|
337
|
+
parseHTML: (el) => coerceAlertType(el.getAttribute('data-alert-type')),
|
|
338
|
+
renderHTML: (attrs) => ({ 'data-alert-type': coerceAlertType(attrs['type']) }),
|
|
339
|
+
},
|
|
340
|
+
icon: {
|
|
341
|
+
default: '',
|
|
342
|
+
parseHTML: (el) => el.getAttribute('data-icon') ?? '',
|
|
343
|
+
renderHTML: (attrs) => (attrs['icon'] ? { 'data-icon': String(attrs['icon']) } : {}),
|
|
344
|
+
},
|
|
345
|
+
// User-pasted custom SVG (sanitized). Wins over `icon` when set; not
|
|
346
|
+
// carried in markdown (would bloat the source) — rich-text only.
|
|
347
|
+
iconSvg: {
|
|
348
|
+
default: '',
|
|
349
|
+
parseHTML: (el) => el.getAttribute('data-icon-svg') ?? '',
|
|
350
|
+
renderHTML: (attrs) => (attrs['iconSvg'] ? { 'data-icon-svg': String(attrs['iconSvg']) } : {}),
|
|
351
|
+
},
|
|
352
|
+
color: {
|
|
353
|
+
default: '',
|
|
354
|
+
parseHTML: (el) => el.getAttribute('data-color') ?? '',
|
|
355
|
+
renderHTML: (attrs) => (attrs['color'] ? { 'data-color': String(attrs['color']) } : {}),
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
parseHTML() {
|
|
360
|
+
return [{ tag: 'div[data-type="alert"]' }];
|
|
361
|
+
},
|
|
362
|
+
renderHTML({ HTMLAttributes }) {
|
|
363
|
+
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'alert', class: 'pilotiq-alert' }), 0];
|
|
364
|
+
},
|
|
365
|
+
addNodeView() {
|
|
366
|
+
return ReactNodeViewRenderer(AlertNodeView);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
/** The Alert heading — a single editable line, defaults to the variant label. */
|
|
370
|
+
export const AlertTitle = Node.create({
|
|
371
|
+
name: 'alertTitle',
|
|
372
|
+
content: 'inline*',
|
|
373
|
+
defining: true,
|
|
374
|
+
parseHTML() {
|
|
375
|
+
return [
|
|
376
|
+
{ tag: 'div[data-type="alertTitle"]' },
|
|
377
|
+
{ tag: '.pilotiq-alert-title' },
|
|
378
|
+
// Back-compat: the pre-redesign alert's non-editable label.
|
|
379
|
+
{ tag: '[data-type="alert"] > .pilotiq-block-label' },
|
|
380
|
+
];
|
|
381
|
+
},
|
|
382
|
+
renderHTML({ HTMLAttributes }) {
|
|
383
|
+
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'alertTitle', class: 'pilotiq-alert-title' }), 0];
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
/** The Alert body — the editable description (`block+`). */
|
|
387
|
+
export const AlertBody = Node.create({
|
|
388
|
+
name: 'alertBody',
|
|
389
|
+
content: 'block+',
|
|
390
|
+
defining: true,
|
|
391
|
+
parseHTML() {
|
|
392
|
+
return [
|
|
393
|
+
{ tag: 'div[data-type="alertBody"]' },
|
|
394
|
+
{ tag: '.pilotiq-alert-description' },
|
|
395
|
+
// Back-compat: the pre-redesign alert's body wrapper.
|
|
396
|
+
{ tag: '[data-type="alert"] > .pilotiq-block-body' },
|
|
397
|
+
];
|
|
398
|
+
},
|
|
399
|
+
renderHTML({ HTMLAttributes }) {
|
|
400
|
+
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'alertBody', class: 'pilotiq-alert-description' }), 0];
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
// ── Pros & cons — two labelled columns (each a `block+` body, list by default) ──
|
|
404
|
+
export const ProsCons = Node.create({
|
|
405
|
+
name: 'prosCons',
|
|
406
|
+
group: 'block',
|
|
407
|
+
content: 'prosColumn consColumn',
|
|
408
|
+
defining: true,
|
|
409
|
+
parseHTML() {
|
|
410
|
+
return [{ tag: 'div[data-type="prosCons"]' }];
|
|
411
|
+
},
|
|
412
|
+
renderHTML({ HTMLAttributes }) {
|
|
413
|
+
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'prosCons', class: 'pilotiq-pros-cons' }), 0];
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
function prosConsColumn(name, label, cssClass) {
|
|
417
|
+
return Node.create({
|
|
418
|
+
name,
|
|
419
|
+
group: 'prosConsColumn',
|
|
420
|
+
content: 'block+',
|
|
421
|
+
defining: true,
|
|
422
|
+
parseHTML() {
|
|
423
|
+
return [{ tag: `div[data-type="${name}"]`, contentElement: '.pilotiq-block-body' }];
|
|
424
|
+
},
|
|
425
|
+
renderHTML({ HTMLAttributes }) {
|
|
426
|
+
return [
|
|
427
|
+
'div',
|
|
428
|
+
mergeAttributes(HTMLAttributes, { 'data-type': name, class: cssClass }),
|
|
429
|
+
['div', { class: 'pilotiq-block-label', contenteditable: 'false' }, label],
|
|
430
|
+
['div', { class: 'pilotiq-block-body' }, 0],
|
|
431
|
+
];
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
export const ProsColumn = prosConsColumn('prosColumn', 'Pros', 'pilotiq-pros');
|
|
436
|
+
export const ConsColumn = prosConsColumn('consColumn', 'Cons', 'pilotiq-cons');
|
|
437
|
+
// ── Keyboard: delete a whole content block with Backspace at its start ──
|
|
438
|
+
//
|
|
439
|
+
// Custom blocks have no inline "remove" affordance, and Backspace inside a
|
|
440
|
+
// nested block (FAQ etc.) just deletes characters. So: Backspace at the very
|
|
441
|
+
// start of a content block removes the entire block — the reliable, keyboard
|
|
442
|
+
// way to delete one (the drag handle's click-to-select is the mouse way).
|
|
443
|
+
// faq is excluded — it handles Backspace itself (empty-question → remove item).
|
|
444
|
+
const DELETABLE_BLOCKS = new Set(['summary', 'keyTakeaways', 'alert', 'prosCons']);
|
|
445
|
+
export const ContentBlockKeymap = Extension.create({
|
|
446
|
+
name: 'pilotiqContentBlockKeymap',
|
|
447
|
+
addKeyboardShortcuts() {
|
|
448
|
+
return {
|
|
449
|
+
Backspace: ({ editor }) => {
|
|
450
|
+
const { $from, empty } = editor.state.selection;
|
|
451
|
+
if (!empty || $from.parentOffset !== 0)
|
|
452
|
+
return false;
|
|
453
|
+
for (let d = 1; d <= $from.depth; d++) {
|
|
454
|
+
const node = $from.node(d);
|
|
455
|
+
if (!DELETABLE_BLOCKS.has(node.type.name))
|
|
456
|
+
continue;
|
|
457
|
+
// Only when the cursor is at the block's very start (first child all
|
|
458
|
+
// the way down) — otherwise let Backspace delete normally.
|
|
459
|
+
let atStart = true;
|
|
460
|
+
for (let dd = $from.depth; dd > d; dd--) {
|
|
461
|
+
if ($from.index(dd - 1) !== 0) {
|
|
462
|
+
atStart = false;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (!atStart)
|
|
467
|
+
return false;
|
|
468
|
+
const start = $from.before(d);
|
|
469
|
+
return editor.chain().deleteRange({ from: start, to: start + node.nodeSize }).focus().run();
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
/** All inline content-block extensions — registered in the editor's list. */
|
|
477
|
+
export const contentBlockNodes = [
|
|
478
|
+
KeyTakeaways,
|
|
479
|
+
Summary,
|
|
480
|
+
Faq,
|
|
481
|
+
FaqItem,
|
|
482
|
+
FaqQuestion,
|
|
483
|
+
FaqAnswer,
|
|
484
|
+
Alert,
|
|
485
|
+
AlertTitle,
|
|
486
|
+
AlertBody,
|
|
487
|
+
ProsCons,
|
|
488
|
+
ProsColumn,
|
|
489
|
+
ConsColumn,
|
|
490
|
+
ContentBlockKeymap,
|
|
491
|
+
];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { RichTextField, DEFAULT_TOOLBAR_GROUPS, DEFAULT_TEXT_COLORS, DEFAULT_HIGHLIGHT_COLORS, type ColorSwatch, type RichTextAttachmentVisibility, type RichTextFieldMeta, type RichTextStorage, type ToolbarButtonId, type ToolbarGroups, } from './RichTextField.js';
|
|
2
2
|
export { Block, type BlockMeta } from './Block.js';
|
|
3
|
+
export { defaultBlocks, faqBlock, alertBlock, summaryBlock, keyTakeawaysBlock, prosConsBlock, } from './blocks/index.js';
|
|
3
4
|
export { MentionProvider, type MentionItem, type MentionProviderMeta, } from './MentionProvider.js';
|
|
4
5
|
export { registerTiptap } from './register.js';
|
|
5
6
|
export { createPlainTextEditor, plainTextOf, plainTextToDoc, type PlainTextEditorOptions, } from './PlainTextEditor.js';
|