@oix1987/yjd 2.0.0 → 2.1.1

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/index.d.ts CHANGED
@@ -2,6 +2,100 @@
2
2
  // These declarations are the package entry types (referenced via "types" in package.json),
3
3
  // so they are declared at top level rather than wrapped in `declare module`.
4
4
 
5
+ /** A person/task suggestion returned by a mention source. */
6
+ export interface MentionItem {
7
+ id: string | number;
8
+ name?: string;
9
+ label?: string;
10
+ avatar_url?: string;
11
+ [key: string]: any;
12
+ }
13
+
14
+ /** Config for a single mention trigger character. */
15
+ export interface MentionTrigger {
16
+ /** Trigger character, e.g. '#'. */
17
+ char: string;
18
+ /** Async (or sync) lookup of suggestions for the typed query. */
19
+ source: (query: string) => MentionItem[] | Promise<MentionItem[]>;
20
+ /** Custom HTML for a suggestion row. Defaults to avatar + name. */
21
+ renderItem?: (item: MentionItem) => string;
22
+ }
23
+
24
+ /** @mention configuration. The token inserted carries `data-id`. */
25
+ export interface MentionOptions {
26
+ /** Primary trigger character (default '@'). */
27
+ trigger?: string;
28
+ source?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
29
+ renderItem?: (item: MentionItem) => string;
30
+ /** Additional triggers, e.g. '#' for task references. */
31
+ triggers?: MentionTrigger[];
32
+ }
33
+
34
+ /** Image upload hook configuration. */
35
+ export interface ImageOptions {
36
+ /**
37
+ * Upload the chosen file and resolve to its URL. While pending, a placeholder
38
+ * is shown; on resolve the src is swapped, on reject the image is removed.
39
+ * Omit to fall back to inline base64 (data URLs).
40
+ */
41
+ upload?: (file: File) => string | Promise<string>;
42
+ /** `accept` attribute for the file picker (default 'image/*'). */
43
+ accept?: string;
44
+ /** Maximum file size in bytes; larger files emit 'image:error'. */
45
+ maxSize?: number;
46
+ }
47
+
48
+ /** Result of a file.upload hook — a URL, or richer metadata. */
49
+ export interface FileUploadResult {
50
+ url: string;
51
+ name?: string;
52
+ size?: string;
53
+ }
54
+
55
+ /** Attachment (non-image file) upload hook. Inserts a "file chip". */
56
+ export interface FileOptions {
57
+ /**
58
+ * Upload the chosen file; resolve to a URL string or { url, name, size }.
59
+ * While pending a placeholder chip is shown. Omit to inline a data: URL.
60
+ */
61
+ upload?: (file: File) => string | FileUploadResult | Promise<string | FileUploadResult>;
62
+ /** `accept` attribute for the file picker (e.g. '.pdf,.zip,.docx'). */
63
+ accept?: string;
64
+ /** Maximum file size in bytes; larger files emit 'file:error'. */
65
+ maxSize?: number;
66
+ }
67
+
68
+ /** Enter-to-submit behaviour (e.g. a comment box). */
69
+ export interface SubmitOptions {
70
+ /**
71
+ * Called when Enter is pressed and no autocomplete popup (mention/slash/emoji)
72
+ * is open. Receives the current HTML and the editor instance.
73
+ */
74
+ onEnter: (html: string, editor: Editor) => void;
75
+ /** Shift+Enter inserts a newline (default true). */
76
+ newlineOnShiftEnter?: boolean;
77
+ }
78
+
79
+ /**
80
+ * Toolbar configuration: a built-in preset, an exclusion of default items,
81
+ * a flat item list (single group), or full custom groups via toolbar1/toolbar2.
82
+ */
83
+ export type ToolbarOption = 'full' | 'compact' | { exclude: string[] } | string[];
84
+
85
+ /** A JSON document node produced by getJSON()/domToJson. */
86
+ export interface JsonNode {
87
+ tag?: string;
88
+ text?: string;
89
+ attrs?: Record<string, string>;
90
+ content?: JsonNode[];
91
+ }
92
+
93
+ /** Root JSON document. */
94
+ export interface JsonDoc {
95
+ type: 'doc';
96
+ content: JsonNode[];
97
+ }
98
+
5
99
  // Editor options interface
6
100
  export interface EditorOptions {
7
101
  placeholder?: string;
@@ -24,6 +118,18 @@ export interface EditorOptions {
24
118
  markdown?: boolean;
25
119
  /** Autosave drafts to localStorage. true, or { key, debounce(ms) }. */
26
120
  autosave?: boolean | { key?: string; debounce?: number };
121
+ /** Image upload hook (replaces inline base64 when `upload` is provided). */
122
+ image?: ImageOptions | boolean;
123
+ /** Attachment (non-image file) upload hook → inserts a file chip. */
124
+ file?: FileOptions;
125
+ /** @mention / #task autocomplete. Inert until a `source` is given. */
126
+ mention?: MentionOptions;
127
+ /** Enter-to-submit behaviour for comment-style editors. */
128
+ submit?: SubmitOptions;
129
+ /** Built-in preset / exclusion / flat list, instead of toolbar1/toolbar2. */
130
+ toolbar?: ToolbarOption;
131
+ /** Warn (emit 'content:overflow') when serialized HTML exceeds this many chars. */
132
+ maxContentSize?: number;
27
133
  features?: {
28
134
  emoji?: boolean;
29
135
  image?: boolean;
@@ -43,11 +149,23 @@ export interface EditorOptions {
43
149
 
44
150
  export class Editor {
45
151
  constructor(selector: string | Element, options?: EditorOptions);
152
+ /** The contentEditable element (public — apps may attach listeners to it). */
153
+ editor: HTMLElement;
46
154
  on(event: string, handler: (data: any) => void): void;
155
+ /** Remove a previously-added listener (symmetric with on()). */
47
156
  off(event: string, handler: (data: any) => void): void;
48
157
  emit(event: string, data: any): void;
49
158
  getContent(): string;
50
159
  setContent(content: string): void;
160
+ /** Alias of getContent() / setContent(). */
161
+ getHTML(): string;
162
+ setHTML(html: string): void;
163
+ /** Export/import the document as a JSON tree. */
164
+ getJSON(): JsonDoc;
165
+ setJSON(json: JsonDoc | JsonNode[]): void;
166
+ /** Export/import the document as Markdown (mention ids preserved). */
167
+ getMarkdown(): string;
168
+ setMarkdown(markdown: string): void;
51
169
  getText(): string;
52
170
  isEmpty(): boolean;
53
171
  clear(): void;
@@ -56,6 +174,12 @@ export class Editor {
56
174
  clearFormatting(): void;
57
175
  insertHorizontalRule(): void;
58
176
  insertImageFile(file: File): void;
177
+ /** Insert a non-image File as a file chip (uses options.file.upload). */
178
+ insertFileAttachment(file: File): void;
179
+ /** Open the native picker for a file attachment. */
180
+ openFileAttachmentPicker(): void;
181
+ /** True when a mention/slash/emoji popup that captures Enter is open. */
182
+ isMenuOpen(): boolean;
59
183
  setReadOnly(readOnly: boolean): void;
60
184
  isReadOnly(): boolean;
61
185
  setDirection(dir: 'ltr' | 'rtl'): void;
@@ -76,10 +200,48 @@ export class RichEditor extends Editor {
76
200
  static register(path: string, definition: any, suppressWarning?: boolean): void;
77
201
  static get(path: string): any;
78
202
  static create(selector: string | Element, options?: EditorOptions): RichEditor;
203
+ /**
204
+ * Progressive-enhance a <textarea> into an editor with TWO-WAY sync (editor
205
+ * edits update textarea.value + fire native events; writing textarea.value
206
+ * updates the editor). The returned editor also exposes a controller:
207
+ * getValue()/setValue()/destroy() (destroy restores the textarea).
208
+ */
209
+ static fromTextarea(
210
+ textarea: HTMLTextAreaElement | string,
211
+ options?: EditorOptions & { format?: 'html' | 'markdown' }
212
+ ): TextareaEditor;
213
+ /** The original textarea, when created via fromTextarea(). */
214
+ textarea?: HTMLTextAreaElement;
79
215
  }
80
216
 
217
+ /** Editor returned by fromTextarea(), with a value controller. */
218
+ export interface TextareaEditor extends RichEditor {
219
+ textarea: HTMLTextAreaElement;
220
+ /** Current content (HTML or Markdown per the `format` option). */
221
+ getValue(): string;
222
+ /** Replace the editor content (HTML or Markdown per `format`). */
223
+ setValue(value: string): void;
224
+ /** Remove the editor and restore the textarea with its last value. */
225
+ destroy(): void;
226
+ }
227
+
228
+ /** Brand-aligned alias of {@link RichEditor}. */
229
+ export { RichEditor as yjd };
230
+
81
231
  export function createEditor(selector: string | Element, options?: EditorOptions): RichEditor;
82
232
 
233
+ /**
234
+ * Render stored HTML into a read-only view that matches the editor's styling.
235
+ * Sanitizes the HTML and tags the host element with `.yjd-content`.
236
+ */
237
+ export function renderStatic(html: string, target?: Element): Element;
238
+
239
+ // Serialization helpers (also available on the editor as get/set methods)
240
+ export function htmlToMarkdown(html: string): string;
241
+ export function markdownToHtml(markdown: string): string;
242
+ export function domToJson(html: string): JsonDoc;
243
+ export function jsonToHtml(json: JsonDoc | JsonNode[]): string;
244
+
83
245
  // Formats
84
246
  export const Bold: any;
85
247
  export const Italic: any;
@@ -115,6 +277,7 @@ export const TableToolbar: any;
115
277
  export const CodeView: any;
116
278
  export const FindReplace: any;
117
279
  export const SlashMenu: any;
280
+ export const Mention: any;
118
281
  export const ResizeHandles: any;
119
282
 
120
283
  // UI components
package/index.js CHANGED
@@ -3,6 +3,8 @@ import registry from './lib/core/registry.js';
3
3
  import Module from './lib/core/module.js';
4
4
  import { Format, InlineFormat, BlockFormat } from './lib/core/format.js';
5
5
  import StylesLoader from './lib/styles-loader.js';
6
+ import { renderStatic } from './lib/static.js';
7
+ import { htmlToMarkdown, markdownToHtml, domToJson, jsonToHtml } from './lib/serialize.js';
6
8
 
7
9
  // Import formats
8
10
  import Bold from './lib/formats/bold.js';
@@ -38,6 +40,7 @@ import TableToolbar from './lib/modules/table-toolbar.js';
38
40
  import CodeView from './lib/modules/code-view.js';
39
41
  import FindReplace from './lib/modules/find-replace.js';
40
42
  import SlashMenu from './lib/modules/slash-menu.js';
43
+ import Mention from './lib/modules/mention.js';
41
44
 
42
45
  import ResizeHandles from './lib/modules/resize-handles.js';
43
46
 
@@ -93,6 +96,7 @@ registry.register('modules/table-toolbar', TableToolbar, true);
93
96
  registry.register('modules/code-view', CodeView, true);
94
97
  registry.register('modules/find-replace', FindReplace, true);
95
98
  registry.register('modules/slash-menu', SlashMenu, true);
99
+ registry.register('modules/mention', Mention, true);
96
100
 
97
101
  registry.register('modules/resize-handles', ResizeHandles, true);
98
102
 
@@ -142,11 +146,109 @@ class RichEditor extends Editor {
142
146
  static create(selector, options = {}) {
143
147
  return new RichEditor(selector, options);
144
148
  }
149
+
150
+ /**
151
+ * Progressive-enhance a <textarea>: hide it, mount an editor in its place,
152
+ * and keep the textarea's value in sync so existing form submits keep working.
153
+ *
154
+ * const ed = RichEditor.fromTextarea(document.querySelector('#body'), {
155
+ * // any editor option; `format` chooses how the textarea is read/written:
156
+ * format: 'html' | 'markdown', // default 'html'
157
+ * });
158
+ *
159
+ * The textarea's current value seeds the editor (parsed as HTML or Markdown
160
+ * per `format`). Binding is TWO-WAY: editor edits update textarea.value (and
161
+ * fire native input/change events), and writing `textarea.value = …` from app
162
+ * code (e.g. resetting a form) updates the editor. The returned editor also
163
+ * carries a small controller:
164
+ *
165
+ * const ed = RichEditor.fromTextarea('#body', { format: 'markdown' });
166
+ * ed.setValue(md); // load new content into the editor
167
+ * ed.getValue(); // current content (html or markdown per `format`)
168
+ * ed.destroy(); // remove the editor, restore the textarea + last value
169
+ *
170
+ * @param {HTMLTextAreaElement|string} textarea Element or selector.
171
+ * @param {object} [options] Editor options + optional `format`.
172
+ * @returns {RichEditor}
173
+ */
174
+ static fromTextarea(textarea, options = {}) {
175
+ const ta = typeof textarea === 'string' ? document.querySelector(textarea) : textarea;
176
+ if (!ta) throw new Error('RichEditor.fromTextarea: textarea not found');
177
+
178
+ const format = options.format === 'markdown' ? 'markdown' : 'html';
179
+ const read = (ed) => (format === 'markdown' ? ed.getMarkdown() : ed.getContent());
180
+ const write = (ed, v) => (format === 'markdown' ? ed.setMarkdown(v || '') : ed.setHTML(v || ''));
181
+
182
+ // Mount point right after the textarea; hide the original.
183
+ const mount = document.createElement('div');
184
+ ta.after(mount);
185
+ ta.style.display = 'none';
186
+ ta.setAttribute('aria-hidden', 'true');
187
+
188
+ // Take over textarea.value so app writes flow into the editor and reads
189
+ // reflect it. Keep the native descriptor to restore on destroy + to set the
190
+ // real value (for form submission) without re-triggering our setter.
191
+ const nativeDesc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
192
+ let raw = ta.value || '';
193
+ let syncing = false; // guards editor→textarea writes from re-entering setValue
194
+
195
+ const initial = raw;
196
+ const editor = new RichEditor(mount, {
197
+ width: '100%',
198
+ ...options,
199
+ content: options.content != null ? options.content
200
+ : (format === 'markdown' ? markdownToHtml(initial) : initial),
201
+ });
202
+
203
+ Object.defineProperty(ta, 'value', {
204
+ configurable: true,
205
+ get() { return raw; },
206
+ set(v) {
207
+ raw = v == null ? '' : String(v);
208
+ nativeDesc.set.call(ta, raw); // keep the real textarea value for submits
209
+ if (!syncing) write(editor, raw); // app write → update editor
210
+ },
211
+ });
212
+
213
+ // editor edit → push to textarea + fire native events for app bindings.
214
+ const onChange = () => {
215
+ const next = read(editor);
216
+ if (raw === next) return;
217
+ raw = next;
218
+ syncing = true;
219
+ nativeDesc.set.call(ta, next);
220
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
221
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
222
+ syncing = false;
223
+ };
224
+ editor.on('change', onChange);
225
+ onChange(); // normalise textarea to the editor's serialization up front.
226
+
227
+ // Controller surface on the editor instance.
228
+ editor.textarea = ta;
229
+ editor.getValue = () => read(editor);
230
+ editor.setValue = (v) => { write(editor, v); };
231
+ const baseDestroy = editor.destroy.bind(editor);
232
+ editor.destroy = () => {
233
+ const last = read(editor);
234
+ editor.off('change', onChange);
235
+ baseDestroy();
236
+ mount.remove();
237
+ delete ta.value; // restore the prototype's value accessor
238
+ nativeDesc.set.call(ta, last);
239
+ ta.style.display = '';
240
+ ta.removeAttribute('aria-hidden');
241
+ };
242
+ return editor;
243
+ }
145
244
  }
146
245
 
147
- // Export classes for extension
246
+ // Export classes for extension. `yjd` is the brand-aligned name; `RichEditor`
247
+ // is kept as an alias for backward compatibility.
148
248
  export {
149
249
  RichEditor as default,
250
+ RichEditor,
251
+ RichEditor as yjd,
150
252
  Editor,
151
253
  Module,
152
254
  Format,
@@ -194,6 +296,7 @@ export {
194
296
  CodeView,
195
297
  FindReplace,
196
298
  SlashMenu,
299
+ Mention,
197
300
 
198
301
  ResizeHandles
199
302
  };
@@ -214,6 +317,15 @@ export {
214
317
  createCustomButton
215
318
  };
216
319
 
320
+ // Static rendering + serialization helpers
321
+ export {
322
+ renderStatic,
323
+ htmlToMarkdown,
324
+ markdownToHtml,
325
+ domToJson,
326
+ jsonToHtml
327
+ };
328
+
217
329
 
218
330
 
219
331