UNPKG

19.8 kBJavaScriptView Raw
1"use strict";
2var __extends = (this && this.__extends) || (function () {
3 var extendStatics = function (d, b) {
4 extendStatics = Object.setPrototypeOf ||
5 ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7 return extendStatics(d, b);
8 };
9 return function (d, b) {
10 if (typeof b !== "function" && b !== null)
11 throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12 extendStatics(d, b);
13 function __() { this.constructor = d; }
14 d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15 };
16})();
17var __assign = (this && this.__assign) || function () {
18 __assign = Object.assign || function(t) {
19 for (var s, i = 1, n = arguments.length; i < n; i++) {
20 s = arguments[i];
21 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
22 t[p] = s[p];
23 }
24 return t;
25 };
26 return __assign.apply(this, arguments);
27};
28Object.defineProperty(exports, "__esModule", { value: true });
29exports.Editor = void 0;
30var React = require("react");
31var ScriptLoader2_1 = require("../ScriptLoader2");
32var TinyMCE_1 = require("../TinyMCE");
33var Utils_1 = require("../Utils");
34var EditorPropTypes_1 = require("./EditorPropTypes");
35var Editor = /** @class */ (function (_super) {
36 __extends(Editor, _super);
37 function Editor(props) {
38 var _this = this;
39 var _a, _b, _c;
40 _this = _super.call(this, props) || this;
41 _this.rollbackTimer = undefined;
42 _this.valueCursor = undefined;
43 _this.rollbackChange = function () {
44 var editor = _this.editor;
45 var value = _this.props.value;
46 if (editor && value && value !== _this.currentContent) {
47 editor.undoManager.ignore(function () {
48 editor.setContent(value);
49 // only restore cursor on inline editors when they are focused
50 // as otherwise it will cause a focus grab
51 if (_this.valueCursor && (!_this.inline || editor.hasFocus())) {
52 try {
53 editor.selection.moveToBookmark(_this.valueCursor);
54 }
55 catch (e) { /* ignore */ }
56 }
57 });
58 }
59 _this.rollbackTimer = undefined;
60 };
61 _this.handleBeforeInput = function (_evt) {
62 if (_this.props.value !== undefined && _this.props.value === _this.currentContent && _this.editor) {
63 if (!_this.inline || _this.editor.hasFocus()) {
64 try {
65 // getBookmark throws exceptions when the editor has not been focused
66 // possibly only in inline mode but I'm not taking chances
67 _this.valueCursor = _this.editor.selection.getBookmark(3);
68 }
69 catch (e) { /* ignore */ }
70 }
71 }
72 };
73 _this.handleBeforeInputSpecial = function (evt) {
74 if (evt.key === 'Enter' || evt.key === 'Backspace' || evt.key === 'Delete') {
75 _this.handleBeforeInput(evt);
76 }
77 };
78 _this.handleEditorChange = function (_evt) {
79 var editor = _this.editor;
80 if (editor && editor.initialized) {
81 var newContent = editor.getContent();
82 if (_this.props.value !== undefined && _this.props.value !== newContent && _this.props.rollback !== false) {
83 // start a timer and revert to the value if not applied in time
84 if (!_this.rollbackTimer) {
85 _this.rollbackTimer = window.setTimeout(_this.rollbackChange, typeof _this.props.rollback === 'number' ? _this.props.rollback : 200);
86 }
87 }
88 if (newContent !== _this.currentContent) {
89 _this.currentContent = newContent;
90 if ((0, Utils_1.isFunction)(_this.props.onEditorChange)) {
91 _this.props.onEditorChange(newContent, editor);
92 }
93 }
94 }
95 };
96 _this.handleEditorChangeSpecial = function (evt) {
97 if (evt.key === 'Backspace' || evt.key === 'Delete') {
98 _this.handleEditorChange(evt);
99 }
100 };
101 _this.initialise = function (attempts) {
102 var _a, _b, _c;
103 if (attempts === void 0) { attempts = 0; }
104 var target = _this.elementRef.current;
105 if (!target) {
106 return; // Editor has been unmounted
107 }
108 if (!(0, Utils_1.isInDoc)(target)) {
109 // this is probably someone trying to help by rendering us offscreen
110 // but we can't do that because the editor iframe must be in the document
111 // in order to have state
112 if (attempts === 0) {
113 // we probably just need to wait for the current events to be processed
114 setTimeout(function () { return _this.initialise(1); }, 1);
115 }
116 else if (attempts < 100) {
117 // wait for ten seconds, polling every tenth of a second
118 setTimeout(function () { return _this.initialise(attempts + 1); }, 100);
119 }
120 else {
121 // give up, at this point it seems that more polling is unlikely to help
122 throw new Error('tinymce can only be initialised when in a document');
123 }
124 return;
125 }
126 var tinymce = (0, TinyMCE_1.getTinymce)(_this.view);
127 if (!tinymce) {
128 throw new Error('tinymce should have been loaded into global scope');
129 }
130 var finalInit = __assign(__assign({}, _this.props.init), { selector: undefined, target: target, readonly: _this.props.disabled, inline: _this.inline, plugins: (0, Utils_1.mergePlugins)((_a = _this.props.init) === null || _a === void 0 ? void 0 : _a.plugins, _this.props.plugins), toolbar: (_b = _this.props.toolbar) !== null && _b !== void 0 ? _b : (_c = _this.props.init) === null || _c === void 0 ? void 0 : _c.toolbar, setup: function (editor) {
131 _this.editor = editor;
132 _this.bindHandlers({});
133 // When running in inline mode the editor gets the initial value
134 // from the innerHTML of the element it is initialized on.
135 // However we don't want to take on the responsibility of sanitizing
136 // to remove XSS in the react integration so we have a chicken and egg
137 // problem... We avoid it by sneaking in a set content before the first
138 // "official" setContent and using TinyMCE to do the sanitization.
139 if (_this.inline && !(0, Utils_1.isTextareaOrInput)(target)) {
140 editor.once('PostRender', function (_evt) {
141 editor.setContent(_this.getInitialValue(), { no_events: true });
142 });
143 }
144 if (_this.props.init && (0, Utils_1.isFunction)(_this.props.init.setup)) {
145 _this.props.init.setup(editor);
146 }
147 }, init_instance_callback: function (editor) {
148 var _a, _b;
149 // check for changes that happened since tinymce.init() was called
150 var initialValue = _this.getInitialValue();
151 _this.currentContent = (_a = _this.currentContent) !== null && _a !== void 0 ? _a : editor.getContent();
152 if (_this.currentContent !== initialValue) {
153 _this.currentContent = initialValue;
154 // same as resetContent in TinyMCE 5
155 editor.setContent(initialValue);
156 editor.undoManager.clear();
157 editor.undoManager.add();
158 editor.setDirty(false);
159 }
160 var disabled = (_b = _this.props.disabled) !== null && _b !== void 0 ? _b : false;
161 (0, Utils_1.setMode)(_this.editor, disabled ? 'readonly' : 'design');
162 // ensure existing init_instance_callback is called
163 if (_this.props.init && (0, Utils_1.isFunction)(_this.props.init.init_instance_callback)) {
164 _this.props.init.init_instance_callback(editor);
165 }
166 } });
167 if (!_this.inline) {
168 target.style.visibility = '';
169 }
170 if ((0, Utils_1.isTextareaOrInput)(target)) {
171 target.value = _this.getInitialValue();
172 }
173 tinymce.init(finalInit);
174 };
175 _this.id = _this.props.id || (0, Utils_1.uuid)('tiny-react');
176 _this.elementRef = React.createRef();
177 _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;
178 _this.boundHandlers = {};
179 return _this;
180 }
181 Object.defineProperty(Editor.prototype, "view", {
182 get: function () {
183 var _a, _b;
184 return (_b = (_a = this.elementRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument.defaultView) !== null && _b !== void 0 ? _b : window;
185 },
186 enumerable: false,
187 configurable: true
188 });
189 Editor.prototype.componentDidUpdate = function (prevProps) {
190 var _this = this;
191 var _a, _b;
192 if (this.rollbackTimer) {
193 clearTimeout(this.rollbackTimer);
194 this.rollbackTimer = undefined;
195 }
196 if (this.editor) {
197 this.bindHandlers(prevProps);
198 if (this.editor.initialized) {
199 this.currentContent = (_a = this.currentContent) !== null && _a !== void 0 ? _a : this.editor.getContent();
200 if (typeof this.props.initialValue === 'string' && this.props.initialValue !== prevProps.initialValue) {
201 // same as resetContent in TinyMCE 5
202 this.editor.setContent(this.props.initialValue);
203 this.editor.undoManager.clear();
204 this.editor.undoManager.add();
205 this.editor.setDirty(false);
206 }
207 else if (typeof this.props.value === 'string' && this.props.value !== this.currentContent) {
208 var localEditor_1 = this.editor;
209 localEditor_1.undoManager.transact(function () {
210 // inline editors grab focus when restoring selection
211 // so we don't try to keep their selection unless they are currently focused
212 var cursor;
213 if (!_this.inline || localEditor_1.hasFocus()) {
214 try {
215 // getBookmark throws exceptions when the editor has not been focused
216 // possibly only in inline mode but I'm not taking chances
217 cursor = localEditor_1.selection.getBookmark(3);
218 }
219 catch (e) { /* ignore */ }
220 }
221 var valueCursor = _this.valueCursor;
222 localEditor_1.setContent(_this.props.value);
223 if (!_this.inline || localEditor_1.hasFocus()) {
224 for (var _i = 0, _a = [cursor, valueCursor]; _i < _a.length; _i++) {
225 var bookmark = _a[_i];
226 if (bookmark) {
227 try {
228 localEditor_1.selection.moveToBookmark(bookmark);
229 _this.valueCursor = bookmark;
230 break;
231 }
232 catch (e) { /* ignore */ }
233 }
234 }
235 }
236 });
237 }
238 if (this.props.disabled !== prevProps.disabled) {
239 var disabled = (_b = this.props.disabled) !== null && _b !== void 0 ? _b : false;
240 (0, Utils_1.setMode)(this.editor, disabled ? 'readonly' : 'design');
241 }
242 }
243 }
244 };
245 Editor.prototype.componentDidMount = function () {
246 var _this = this;
247 var _a, _b, _c, _d, _e;
248 if ((0, TinyMCE_1.getTinymce)(this.view) !== null) {
249 this.initialise();
250 }
251 else if (Array.isArray(this.props.tinymceScriptSrc) && this.props.tinymceScriptSrc.length === 0) {
252 (_b = (_a = this.props).onScriptsLoadError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error('No `tinymce` global is present but the `tinymceScriptSrc` prop was an empty array.'));
253 }
254 else if ((_c = this.elementRef.current) === null || _c === void 0 ? void 0 : _c.ownerDocument) {
255 var successHandler = function () {
256 var _a, _b;
257 (_b = (_a = _this.props).onScriptsLoad) === null || _b === void 0 ? void 0 : _b.call(_a);
258 _this.initialise();
259 };
260 var errorHandler = function (err) {
261 var _a, _b;
262 (_b = (_a = _this.props).onScriptsLoadError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
263 };
264 ScriptLoader2_1.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);
265 }
266 };
267 Editor.prototype.componentWillUnmount = function () {
268 var _this = this;
269 var editor = this.editor;
270 if (editor) {
271 editor.off(this.changeEvents(), this.handleEditorChange);
272 editor.off(this.beforeInputEvent(), this.handleBeforeInput);
273 editor.off('keypress', this.handleEditorChangeSpecial);
274 editor.off('keydown', this.handleBeforeInputSpecial);
275 editor.off('NewBlock', this.handleEditorChange);
276 Object.keys(this.boundHandlers).forEach(function (eventName) {
277 editor.off(eventName, _this.boundHandlers[eventName]);
278 });
279 this.boundHandlers = {};
280 editor.remove();
281 this.editor = undefined;
282 }
283 };
284 Editor.prototype.render = function () {
285 return this.inline ? this.renderInline() : this.renderIframe();
286 };
287 Editor.prototype.changeEvents = function () {
288 var _a, _b, _c;
289 var isIE = (_c = (_b = (_a = (0, TinyMCE_1.getTinymce)(this.view)) === null || _a === void 0 ? void 0 : _a.Env) === null || _b === void 0 ? void 0 : _b.browser) === null || _c === void 0 ? void 0 : _c.isIE();
290 return (isIE
291 ? 'change keyup compositionend setcontent CommentChange'
292 : 'change input compositionend setcontent CommentChange');
293 };
294 Editor.prototype.beforeInputEvent = function () {
295 return (0, Utils_1.isBeforeInputEventAvailable)() ? 'beforeinput SelectionChange' : 'SelectionChange';
296 };
297 Editor.prototype.renderInline = function () {
298 var _a = this.props.tagName, tagName = _a === void 0 ? 'div' : _a;
299 return React.createElement(tagName, {
300 ref: this.elementRef,
301 id: this.id
302 });
303 };
304 Editor.prototype.renderIframe = function () {
305 return React.createElement('textarea', {
306 ref: this.elementRef,
307 style: { visibility: 'hidden' },
308 name: this.props.textareaName,
309 id: this.id
310 });
311 };
312 Editor.prototype.getScriptSources = function () {
313 var _a, _b;
314 var async = (_a = this.props.scriptLoading) === null || _a === void 0 ? void 0 : _a.async;
315 var defer = (_b = this.props.scriptLoading) === null || _b === void 0 ? void 0 : _b.defer;
316 if (this.props.tinymceScriptSrc !== undefined) {
317 if (typeof this.props.tinymceScriptSrc === 'string') {
318 return [{ src: this.props.tinymceScriptSrc, async: async, defer: defer }];
319 }
320 // multiple scripts can be specified which allows for hybrid mode
321 return this.props.tinymceScriptSrc.map(function (item) {
322 if (typeof item === 'string') {
323 // async does not make sense for multiple items unless
324 // they are not dependent (which will be unlikely)
325 return { src: item, async: async, defer: defer };
326 }
327 else {
328 return item;
329 }
330 });
331 }
332 // fallback to the cloud when the tinymceScriptSrc is not specified
333 var channel = this.props.cloudChannel;
334 var apiKey = this.props.apiKey ? this.props.apiKey : 'no-api-key';
335 var cloudTinyJs = "https://cdn.tiny.cloud/1/".concat(apiKey, "/tinymce/").concat(channel, "/tinymce.min.js");
336 return [{ src: cloudTinyJs, async: async, defer: defer }];
337 };
338 Editor.prototype.getInitialValue = function () {
339 if (typeof this.props.initialValue === 'string') {
340 return this.props.initialValue;
341 }
342 else if (typeof this.props.value === 'string') {
343 return this.props.value;
344 }
345 else {
346 return '';
347 }
348 };
349 Editor.prototype.bindHandlers = function (prevProps) {
350 var _this = this;
351 if (this.editor !== undefined) {
352 // typescript chokes trying to understand the type of the lookup function
353 (0, Utils_1.configHandlers)(this.editor, prevProps, this.props, this.boundHandlers, function (key) { return _this.props[key]; });
354 // check if we should monitor editor changes
355 var isValueControlled = function (p) { return p.onEditorChange !== undefined || p.value !== undefined; };
356 var wasControlled = isValueControlled(prevProps);
357 var nowControlled = isValueControlled(this.props);
358 if (!wasControlled && nowControlled) {
359 this.editor.on(this.changeEvents(), this.handleEditorChange);
360 this.editor.on(this.beforeInputEvent(), this.handleBeforeInput);
361 this.editor.on('keydown', this.handleBeforeInputSpecial);
362 this.editor.on('keyup', this.handleEditorChangeSpecial);
363 this.editor.on('NewBlock', this.handleEditorChange);
364 }
365 else if (wasControlled && !nowControlled) {
366 this.editor.off(this.changeEvents(), this.handleEditorChange);
367 this.editor.off(this.beforeInputEvent(), this.handleBeforeInput);
368 this.editor.off('keydown', this.handleBeforeInputSpecial);
369 this.editor.off('keyup', this.handleEditorChangeSpecial);
370 this.editor.off('NewBlock', this.handleEditorChange);
371 }
372 }
373 };
374 Editor.propTypes = EditorPropTypes_1.EditorPropTypes;
375 Editor.defaultProps = {
376 cloudChannel: '6'
377 };
378 return Editor;
379}(React.Component));
380exports.Editor = Editor;