@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/README.md +140 -6
- package/core.js +5 -0
- package/dist/core.esm.js +1 -1
- package/dist/core.esm.js.map +1 -1
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +163 -0
- package/index.js +113 -1
- package/lib/core/editor.js +313 -14
- package/lib/formats/image.js +16 -22
- package/lib/modules/mention.js +228 -0
- package/lib/modules/slash-menu.js +6 -4
- package/lib/modules/toolbar.js +28 -0
- package/lib/serialize.js +248 -0
- package/lib/static.js +28 -0
- package/lib/styles.css +182 -0
- package/lib/styles.css.js +1 -1
- package/lib/styles.min.css +1 -1
- package/lib/ui/icons.js +1 -0
- package/package.json +1 -1
- package/umd-entry.js +6 -5
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
|
|