truerte-react 0.0.3

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE.txt +22 -0
  3. package/README.md +54 -0
  4. package/lib/cjs/main/ts/EventNames.d.ts +8 -0
  5. package/lib/cjs/main/ts/EventNames.js +87 -0
  6. package/lib/cjs/main/ts/Events.d.ts +94 -0
  7. package/lib/cjs/main/ts/Events.js +2 -0
  8. package/lib/cjs/main/ts/ScriptLoader2.d.ts +10 -0
  9. package/lib/cjs/main/ts/ScriptLoader2.js +138 -0
  10. package/lib/cjs/main/ts/TrueRTE.d.ts +3 -0
  11. package/lib/cjs/main/ts/TrueRTE.js +8 -0
  12. package/lib/cjs/main/ts/Utils.d.ts +18 -0
  13. package/lib/cjs/main/ts/Utils.js +53 -0
  14. package/lib/cjs/main/ts/Uuid.d.ts +6 -0
  15. package/lib/cjs/main/ts/Uuid.js +16 -0
  16. package/lib/cjs/main/ts/components/Editor.d.ts +171 -0
  17. package/lib/cjs/main/ts/components/Editor.js +358 -0
  18. package/lib/cjs/main/ts/index.d.ts +2 -0
  19. package/lib/cjs/main/ts/index.js +5 -0
  20. package/lib/es2015/main/ts/EventNames.d.ts +8 -0
  21. package/lib/es2015/main/ts/EventNames.js +84 -0
  22. package/lib/es2015/main/ts/Events.d.ts +94 -0
  23. package/lib/es2015/main/ts/Events.js +1 -0
  24. package/lib/es2015/main/ts/ScriptLoader2.d.ts +10 -0
  25. package/lib/es2015/main/ts/ScriptLoader2.js +135 -0
  26. package/lib/es2015/main/ts/TrueRTE.d.ts +3 -0
  27. package/lib/es2015/main/ts/TrueRTE.js +5 -0
  28. package/lib/es2015/main/ts/Utils.d.ts +18 -0
  29. package/lib/es2015/main/ts/Utils.js +43 -0
  30. package/lib/es2015/main/ts/Uuid.d.ts +6 -0
  31. package/lib/es2015/main/ts/Uuid.js +12 -0
  32. package/lib/es2015/main/ts/components/Editor.d.ts +171 -0
  33. package/lib/es2015/main/ts/components/Editor.js +354 -0
  34. package/lib/es2015/main/ts/index.d.ts +2 -0
  35. package/lib/es2015/main/ts/index.js +2 -0
  36. package/package.json +63 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Official TrueRTE React component
3
+ * Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
4
+ * Copyright (c) 2024 TrueRTE contributors
5
+ * Licensed under the MIT license (https://github.com/truerte/truerte-react/blob/main/LICENSE.TXT)
6
+ */
7
+ import * as React from 'react';
8
+ import { ScriptLoader } from '../ScriptLoader2';
9
+ import { getTrueRTE } from '../TrueRTE';
10
+ import { isFunction, isTextareaOrInput, mergePlugins, configHandlers, isBeforeInputEventAvailable, setMode } from '../Utils';
11
+ import { uuid } from '../Uuid';
12
+ const changeEvents = 'change keyup compositionend setcontent CommentChange';
13
+ /**
14
+ * @see {@link https://www.truerte.org/docs/truerte/1/react-ref/ TrueRTE React Technical Reference}
15
+ */
16
+ export class Editor extends React.Component {
17
+ constructor(props) {
18
+ var _a, _b, _c;
19
+ super(props);
20
+ this.rollbackTimer = undefined;
21
+ this.valueCursor = undefined;
22
+ this.rollbackChange = () => {
23
+ const editor = this.editor;
24
+ const value = this.props.value;
25
+ if (editor && value && value !== this.currentContent) {
26
+ editor.undoManager.ignore(() => {
27
+ editor.setContent(value);
28
+ // only restore cursor on inline editors when they are focused
29
+ // as otherwise it will cause a focus grab
30
+ if (this.valueCursor && (!this.inline || editor.hasFocus())) {
31
+ try {
32
+ editor.selection.moveToBookmark(this.valueCursor);
33
+ }
34
+ catch (e) { /* ignore */ }
35
+ }
36
+ });
37
+ }
38
+ this.rollbackTimer = undefined;
39
+ };
40
+ this.handleBeforeInput = (_evt) => {
41
+ if (this.props.value !== undefined && this.props.value === this.currentContent && this.editor) {
42
+ if (!this.inline || this.editor.hasFocus()) {
43
+ try {
44
+ // getBookmark throws exceptions when the editor has not been focused
45
+ // possibly only in inline mode but I'm not taking chances
46
+ this.valueCursor = this.editor.selection.getBookmark(3);
47
+ }
48
+ catch (e) { /* ignore */ }
49
+ }
50
+ }
51
+ };
52
+ this.handleBeforeInputSpecial = (evt) => {
53
+ if (evt.key === 'Enter' || evt.key === 'Backspace' || evt.key === 'Delete') {
54
+ this.handleBeforeInput(evt);
55
+ }
56
+ };
57
+ this.handleEditorChange = (_evt) => {
58
+ const editor = this.editor;
59
+ if (editor && editor.initialized) {
60
+ const newContent = editor.getContent();
61
+ if (this.props.value !== undefined && this.props.value !== newContent && this.props.rollback !== false) {
62
+ // start a timer and revert to the value if not applied in time
63
+ if (!this.rollbackTimer) {
64
+ this.rollbackTimer = window.setTimeout(this.rollbackChange, typeof this.props.rollback === 'number' ? this.props.rollback : 200);
65
+ }
66
+ }
67
+ if (newContent !== this.currentContent) {
68
+ this.currentContent = newContent;
69
+ if (isFunction(this.props.onEditorChange)) {
70
+ this.props.onEditorChange(newContent, editor);
71
+ }
72
+ }
73
+ }
74
+ };
75
+ this.handleEditorChangeSpecial = (evt) => {
76
+ if (evt.key === 'Backspace' || evt.key === 'Delete') {
77
+ this.handleEditorChange(evt);
78
+ }
79
+ };
80
+ this.assignEditorRef = (editorRef, editor) => {
81
+ if (editorRef === undefined || editorRef === null) {
82
+ return;
83
+ }
84
+ if (typeof editorRef === 'function') {
85
+ editorRef(editor);
86
+ }
87
+ else {
88
+ editorRef.current = editor;
89
+ }
90
+ };
91
+ this.initialise = (attempts = 0) => {
92
+ var _a, _b, _c, _d, _e, _f;
93
+ const target = this.elementRef.current;
94
+ if (!target) {
95
+ return; // Editor has been unmounted
96
+ }
97
+ if (!target.isConnected) {
98
+ // this is probably someone trying to help by rendering us offscreen
99
+ // but we can't do that because the editor iframe must be in the document
100
+ // in order to have state
101
+ // TODO: how will this do when we use web component?
102
+ if (attempts === 0) {
103
+ // we probably just need to wait for the current events to be processed
104
+ setTimeout(() => this.initialise(1), 1);
105
+ }
106
+ else if (attempts < 100) {
107
+ // wait for ten seconds, polling every tenth of a second
108
+ setTimeout(() => this.initialise(attempts + 1), 100);
109
+ }
110
+ else {
111
+ // give up, at this point it seems that more polling is unlikely to help
112
+ throw new Error('truerte can only be initialised when in a document');
113
+ }
114
+ return;
115
+ }
116
+ const truerte = getTrueRTE(this.view);
117
+ if (!truerte) {
118
+ throw new Error('truerte should have been loaded into global scope');
119
+ }
120
+ const resolvedPlugins = mergePlugins((_a = this.props.init) === null || _a === void 0 ? void 0 : _a.plugins, this.props.plugins);
121
+ const hasExplicitIconConfig = ((_b = this.props.init) === null || _b === void 0 ? void 0 : _b.icons) !== undefined || ((_c = this.props.init) === null || _c === void 0 ? void 0 : _c.icons_url) !== undefined;
122
+ const resolvedIconPack = this.props.useLucideIcons === true && !hasExplicitIconConfig
123
+ ? 'truerte-lucide'
124
+ : (_d = this.props.init) === null || _d === void 0 ? void 0 : _d.icons;
125
+ const finalInit = Object.assign(Object.assign({}, this.props.init), { selector: undefined, target, readonly: this.props.disabled, inline: this.inline, plugins: resolvedPlugins, icons: resolvedIconPack, toolbar: (_e = this.props.toolbar) !== null && _e !== void 0 ? _e : (_f = this.props.init) === null || _f === void 0 ? void 0 : _f.toolbar, setup: (editor) => {
126
+ this.editor = editor;
127
+ this.assignEditorRef(this.props.editorRef, editor);
128
+ this.bindHandlers({});
129
+ // When running in inline mode the editor gets the initial value
130
+ // from the innerHTML of the element it is initialized on.
131
+ // However we don't want to take on the responsibility of sanitizing
132
+ // to remove XSS in the react integration so we have a chicken and egg
133
+ // problem... We avoid it by sneaking in a set content before the first
134
+ // "official" setContent and using TrueRTE to do the sanitization.
135
+ if (this.inline && !isTextareaOrInput(target)) {
136
+ editor.once('PostRender', (_evt) => {
137
+ editor.setContent(this.getInitialValue(), { no_events: true });
138
+ });
139
+ }
140
+ if (this.props.init && isFunction(this.props.init.setup)) {
141
+ this.props.init.setup(editor);
142
+ }
143
+ }, init_instance_callback: (editor) => {
144
+ var _a, _b;
145
+ // check for changes that happened since truerte.init() was called
146
+ const initialValue = this.getInitialValue();
147
+ this.currentContent = (_a = this.currentContent) !== null && _a !== void 0 ? _a : editor.getContent();
148
+ if (this.currentContent !== initialValue) {
149
+ this.currentContent = initialValue;
150
+ // same as resetContent in TrueRTE 5
151
+ editor.setContent(initialValue);
152
+ editor.undoManager.clear();
153
+ editor.undoManager.add();
154
+ editor.setDirty(false);
155
+ }
156
+ const disabled = (_b = this.props.disabled) !== null && _b !== void 0 ? _b : false;
157
+ setMode(this.editor, disabled ? 'readonly' : 'design');
158
+ // ensure existing init_instance_callback is called
159
+ if (this.props.init && isFunction(this.props.init.init_instance_callback)) {
160
+ this.props.init.init_instance_callback(editor);
161
+ }
162
+ } });
163
+ if (!this.inline) {
164
+ target.style.visibility = '';
165
+ }
166
+ if (isTextareaOrInput(target)) {
167
+ target.value = this.getInitialValue();
168
+ }
169
+ truerte.init(finalInit);
170
+ };
171
+ this.id = this.props.id || uuid('truerte-react');
172
+ this.elementRef = React.createRef();
173
+ this.inline = (_c = (_a = this.props.inline) !== null && _a !== void 0 ? _a : (_b = this.props.init) === null || _b === void 0 ? void 0 : _b.inline) !== null && _c !== void 0 ? _c : false;
174
+ this.boundHandlers = {};
175
+ }
176
+ get view() {
177
+ var _a, _b;
178
+ return (_b = (_a = this.elementRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument.defaultView) !== null && _b !== void 0 ? _b : window;
179
+ }
180
+ componentDidUpdate(prevProps) {
181
+ var _a, _b, _c;
182
+ if (this.props.editorRef !== prevProps.editorRef) {
183
+ this.assignEditorRef(prevProps.editorRef, null);
184
+ this.assignEditorRef(this.props.editorRef, (_a = this.editor) !== null && _a !== void 0 ? _a : null);
185
+ }
186
+ if (this.rollbackTimer) {
187
+ clearTimeout(this.rollbackTimer);
188
+ this.rollbackTimer = undefined;
189
+ }
190
+ if (this.editor) {
191
+ this.bindHandlers(prevProps);
192
+ if (this.editor.initialized) {
193
+ this.currentContent = (_b = this.currentContent) !== null && _b !== void 0 ? _b : this.editor.getContent();
194
+ if (typeof this.props.initialValue === 'string' && this.props.initialValue !== prevProps.initialValue) {
195
+ // same as resetContent in TinyMCE 5 – TODO what does this mean for TrueRTE?
196
+ this.editor.setContent(this.props.initialValue);
197
+ this.editor.undoManager.clear();
198
+ this.editor.undoManager.add();
199
+ this.editor.setDirty(false);
200
+ }
201
+ else if (typeof this.props.value === 'string' && this.props.value !== this.currentContent) {
202
+ const localEditor = this.editor;
203
+ localEditor.undoManager.transact(() => {
204
+ // inline editors grab focus when restoring selection
205
+ // so we don't try to keep their selection unless they are currently focused
206
+ let cursor;
207
+ if (!this.inline || localEditor.hasFocus()) {
208
+ try {
209
+ // getBookmark throws exceptions when the editor has not been focused
210
+ // possibly only in inline mode but I'm not taking chances
211
+ cursor = localEditor.selection.getBookmark(3);
212
+ }
213
+ catch (e) { /* ignore */ }
214
+ }
215
+ const valueCursor = this.valueCursor;
216
+ localEditor.setContent(this.props.value);
217
+ if (!this.inline || localEditor.hasFocus()) {
218
+ for (const bookmark of [cursor, valueCursor]) {
219
+ if (bookmark) {
220
+ try {
221
+ localEditor.selection.moveToBookmark(bookmark);
222
+ this.valueCursor = bookmark;
223
+ break;
224
+ }
225
+ catch (e) { /* ignore */ }
226
+ }
227
+ }
228
+ }
229
+ });
230
+ }
231
+ if (this.props.disabled !== prevProps.disabled) {
232
+ const disabled = (_c = this.props.disabled) !== null && _c !== void 0 ? _c : false;
233
+ setMode(this.editor, disabled ? 'readonly' : 'design');
234
+ }
235
+ }
236
+ }
237
+ }
238
+ componentDidMount() {
239
+ var _a, _b, _c, _d, _e;
240
+ if (getTrueRTE(this.view) !== null) {
241
+ this.initialise();
242
+ }
243
+ else if (Array.isArray(this.props.truerteScriptSrc) && this.props.truerteScriptSrc.length === 0) {
244
+ (_b = (_a = this.props).onScriptsLoadError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error('No `truerte` global is present but the `truerteScriptSrc` prop was an empty array.'));
245
+ }
246
+ else if ((_c = this.elementRef.current) === null || _c === void 0 ? void 0 : _c.ownerDocument) {
247
+ const successHandler = () => {
248
+ var _a, _b;
249
+ (_b = (_a = this.props).onScriptsLoad) === null || _b === void 0 ? void 0 : _b.call(_a);
250
+ this.initialise();
251
+ };
252
+ const errorHandler = (err) => {
253
+ var _a, _b;
254
+ (_b = (_a = this.props).onScriptsLoadError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
255
+ };
256
+ ScriptLoader.loadList(this.elementRef.current.ownerDocument, this.getScriptSources(), (_e = (_d = this.props.scriptLoading) === null || _d === void 0 ? void 0 : _d.delay) !== null && _e !== void 0 ? _e : 0, successHandler, errorHandler);
257
+ }
258
+ }
259
+ componentWillUnmount() {
260
+ const editor = this.editor;
261
+ if (editor) {
262
+ editor.off(changeEvents, this.handleEditorChange);
263
+ editor.off(this.beforeInputEvent(), this.handleBeforeInput);
264
+ editor.off('keypress', this.handleEditorChangeSpecial);
265
+ editor.off('keydown', this.handleBeforeInputSpecial);
266
+ editor.off('NewBlock', this.handleEditorChange);
267
+ Object.keys(this.boundHandlers).forEach((eventName) => {
268
+ editor.off(eventName, this.boundHandlers[eventName]);
269
+ });
270
+ this.boundHandlers = {};
271
+ editor.remove();
272
+ this.editor = undefined;
273
+ }
274
+ this.assignEditorRef(this.props.editorRef, null);
275
+ }
276
+ render() {
277
+ return this.inline ? this.renderInline() : this.renderIframe();
278
+ }
279
+ getEditor() {
280
+ return this.editor;
281
+ }
282
+ beforeInputEvent() {
283
+ return isBeforeInputEventAvailable() ? 'beforeinput SelectionChange' : 'SelectionChange';
284
+ }
285
+ renderInline() {
286
+ const { tagName = 'div' } = this.props;
287
+ return React.createElement(tagName, {
288
+ ref: this.elementRef,
289
+ id: this.id,
290
+ tabIndex: this.props.tabIndex
291
+ });
292
+ }
293
+ renderIframe() {
294
+ return React.createElement('textarea', {
295
+ ref: this.elementRef,
296
+ style: { visibility: 'hidden' },
297
+ name: this.props.textareaName,
298
+ id: this.id,
299
+ tabIndex: this.props.tabIndex
300
+ });
301
+ }
302
+ getScriptSources() {
303
+ var _a, _b;
304
+ const async = (_a = this.props.scriptLoading) === null || _a === void 0 ? void 0 : _a.async;
305
+ const defer = (_b = this.props.scriptLoading) === null || _b === void 0 ? void 0 : _b.defer;
306
+ if (this.props.truerteScriptSrc !== undefined) {
307
+ if (typeof this.props.truerteScriptSrc === 'string') {
308
+ return [{ src: this.props.truerteScriptSrc, async, defer }];
309
+ }
310
+ }
311
+ // fallback to jsDelivr CDN when the truerteScriptSrc is not specified
312
+ // `cdnVersion` is in `defaultProps`, so it's always defined.
313
+ const cdnLink = `https://cdn.jsdelivr.net/npm/truerte@${this.props.cdnVersion}/truerte.min.js`;
314
+ return [{ src: cdnLink, async, defer }];
315
+ }
316
+ getInitialValue() {
317
+ if (typeof this.props.initialValue === 'string') {
318
+ return this.props.initialValue;
319
+ }
320
+ else if (typeof this.props.value === 'string') {
321
+ return this.props.value;
322
+ }
323
+ else {
324
+ return '';
325
+ }
326
+ }
327
+ bindHandlers(prevProps) {
328
+ if (this.editor !== undefined) {
329
+ // typescript chokes trying to understand the type of the lookup function
330
+ configHandlers(this.editor, prevProps, this.props, this.boundHandlers, (key) => this.props[key]);
331
+ // check if we should monitor editor changes
332
+ const isValueControlled = (p) => p.onEditorChange !== undefined || p.value !== undefined;
333
+ const wasControlled = isValueControlled(prevProps);
334
+ const nowControlled = isValueControlled(this.props);
335
+ if (!wasControlled && nowControlled) {
336
+ this.editor.on(changeEvents, this.handleEditorChange);
337
+ this.editor.on(this.beforeInputEvent(), this.handleBeforeInput);
338
+ this.editor.on('keydown', this.handleBeforeInputSpecial);
339
+ this.editor.on('keyup', this.handleEditorChangeSpecial);
340
+ this.editor.on('NewBlock', this.handleEditorChange);
341
+ }
342
+ else if (wasControlled && !nowControlled) {
343
+ this.editor.off(changeEvents, this.handleEditorChange);
344
+ this.editor.off(this.beforeInputEvent(), this.handleBeforeInput);
345
+ this.editor.off('keydown', this.handleBeforeInputSpecial);
346
+ this.editor.off('keyup', this.handleEditorChangeSpecial);
347
+ this.editor.off('NewBlock', this.handleEditorChange);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ Editor.defaultProps = {
353
+ cdnVersion: '1',
354
+ };
@@ -0,0 +1,2 @@
1
+ import { Editor, EditorInstanceRef, IAllProps, IProps, InitOptions } from './components/Editor';
2
+ export { Editor, IAllProps, IProps, InitOptions, EditorInstanceRef };
@@ -0,0 +1,2 @@
1
+ import { Editor } from './components/Editor';
2
+ export { Editor };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "truerte-react",
3
+ "description": "TrueRTE React Component",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/TrueRTE/truerte-react.git"
7
+ },
8
+ "bugs": {
9
+ "url": "https://github.com/TrueRTE/truerte-react/issues"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "LICENSE.txt"
16
+ ],
17
+ "main": "lib/cjs/main/ts/index.js",
18
+ "module": "lib/es2015/main/ts/index.js",
19
+ "scripts": {
20
+ "lint": "eslint 'src/**/*.ts?(x)'",
21
+ "clean": "rimraf lib",
22
+ "test": "bedrock-auto -b chrome-headless -f src/test/ts/**/*Test.ts",
23
+ "test-manual": "bedrock -f src/test/ts/**/*Test.ts",
24
+ "build": "npm run clean && tsc -p ./tsconfig.es2015.json && tsc -p ./tsconfig.cjs.json",
25
+ "watch": "tsc -w -p ./tsconfig.es2015.json",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "truerte",
30
+ "react",
31
+ "editor"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "truerte": "^1.0.0",
36
+ "react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
37
+ "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "truerte": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@ephox/agar": "8.0.1",
46
+ "@ephox/bedrock-client": "^15.0.0",
47
+ "@ephox/bedrock-server": "^15.0.3-alpha.0",
48
+ "@ephox/mcagar": "^9.0.1",
49
+ "@tinymce/eslint-plugin": "^3.0.0",
50
+ "@types/node": "^22.17.2",
51
+ "@types/react": "^19.1.10",
52
+ "@types/react-dom": "^19.1.7",
53
+ "truerte": "file:../truerte/modules/truerte",
54
+ "truerte-1": "file:../truerte/modules/truerte",
55
+ "react": "^19.1.1",
56
+ "react-dom": "^19.1.1",
57
+ "rimraf": "^6.0.1",
58
+ "typescript": "~5.8.3",
59
+ "vite": "^6.3.5"
60
+ },
61
+ "version": "0.0.3",
62
+ "dependencies": {}
63
+ }