@instructure/canvas-rce 7.3.0 → 7.3.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/CHANGELOG.md +14 -0
- package/es/enhance-user-content/doc_previews.js +5 -0
- package/es/enhance-user-content/enhance_user_content.js +1 -1
- package/es/enhance-user-content/instructure_helper.js +1 -0
- package/es/rce/AlertMessageArea.d.ts +2 -2
- package/es/rce/AlertMessageArea.js +3 -3
- package/es/rce/KeyboardShortcutModal.js +1 -1
- package/es/rce/RCEWrapper.d.ts +11 -11
- package/es/rce/RCEWrapper.js +216 -191
- package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.d.ts +1 -1
- package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.js +25 -25
- package/es/rce/plugins/instructure_studio_media_options/plugin.js +4 -2
- package/es/rce/plugins/instructure_wordcount/components/WordCountModal.js +1 -0
- package/es/rce/plugins/shared/StudioLtiSupportUtils.d.ts +2 -1
- package/es/rce/plugins/shared/StudioLtiSupportUtils.js +17 -1
- package/es/translations/locales/ar.js +15 -0
- package/es/translations/locales/ca.js +15 -0
- package/es/translations/locales/cy.js +15 -0
- package/es/translations/locales/da-x-k12.js +15 -0
- package/es/translations/locales/da.js +15 -0
- package/es/translations/locales/de.js +15 -0
- package/es/translations/locales/en-AU-x-unimelb.js +15 -0
- package/es/translations/locales/en-GB-x-ukhe.js +15 -0
- package/es/translations/locales/en.js +6 -0
- package/es/translations/locales/en_AU.js +15 -0
- package/es/translations/locales/en_CA.js +15 -0
- package/es/translations/locales/en_CY.js +15 -0
- package/es/translations/locales/en_GB.js +15 -0
- package/es/translations/locales/es.js +15 -0
- package/es/translations/locales/es_ES.js +15 -0
- package/es/translations/locales/fi.js +15 -0
- package/es/translations/locales/fr.js +15 -0
- package/es/translations/locales/fr_CA.js +15 -0
- package/es/translations/locales/ga.js +15 -0
- package/es/translations/locales/hi.js +15 -0
- package/es/translations/locales/ht.js +15 -0
- package/es/translations/locales/id.js +15 -0
- package/es/translations/locales/is.js +15 -0
- package/es/translations/locales/it.js +15 -0
- package/es/translations/locales/ja.js +15 -0
- package/es/translations/locales/mi.js +15 -0
- package/es/translations/locales/ms.js +15 -0
- package/es/translations/locales/nb-x-k12.js +15 -0
- package/es/translations/locales/nb.js +15 -0
- package/es/translations/locales/nl.js +15 -0
- package/es/translations/locales/pl.js +15 -0
- package/es/translations/locales/pt.js +15 -0
- package/es/translations/locales/pt_BR.js +15 -0
- package/es/translations/locales/ru.js +15 -0
- package/es/translations/locales/sl.js +15 -0
- package/es/translations/locales/sv-x-k12.js +15 -0
- package/es/translations/locales/sv.js +15 -0
- package/es/translations/locales/th.js +15 -0
- package/es/translations/locales/vi.js +15 -0
- package/es/translations/locales/zh-Hans.js +15 -0
- package/es/translations/locales/zh-Hant.js +15 -0
- package/es/translations/locales/zh.js +15 -0
- package/es/translations/locales/zh_HK.js +15 -0
- package/package.json +1 -1
package/es/rce/RCEWrapper.js
CHANGED
|
@@ -17,63 +17,63 @@ import _pt from "prop-types";
|
|
|
17
17
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import React, { Suspense } from
|
|
21
|
-
import { Editor } from
|
|
22
|
-
import tinymce from
|
|
23
|
-
import _ from
|
|
24
|
-
import { StoreProvider } from
|
|
25
|
-
import { IconKeyboardShortcutsLine } from
|
|
26
|
-
import { Alert } from
|
|
27
|
-
import { Spinner } from
|
|
28
|
-
import { View } from
|
|
29
|
-
import { debounce } from
|
|
30
|
-
import { uid } from
|
|
31
|
-
import { FocusRegionManager } from
|
|
32
|
-
import getCookie from
|
|
33
|
-
import formatMessage from
|
|
34
|
-
import * as contentInsertion from
|
|
35
|
-
import indicatorRegion from
|
|
36
|
-
import { editorLanguage } from
|
|
37
|
-
import normalizeLocale from
|
|
38
|
-
import { sanitizePlugins } from
|
|
39
|
-
import RCEGlobals from
|
|
40
|
-
import defaultTinymceConfig from
|
|
41
|
-
import { FS_CHANGEEVENT, FS_ELEMENT, FS_ENABLED, FS_EXIT, FS_REQUEST, instuiPopupMountNodeFn } from
|
|
42
|
-
import indicate from
|
|
43
|
-
import bridge from
|
|
44
|
-
import CanvasContentTray from
|
|
45
|
-
import StatusBar, { PRETTY_HTML_EDITOR_VIEW, RAW_HTML_EDITOR_VIEW, WYSIWYG_VIEW } from
|
|
46
|
-
import { VIEW_CHANGE } from
|
|
47
|
-
import ShowOnFocusButton from
|
|
48
|
-
import KeyboardShortcutModal from
|
|
49
|
-
import AlertMessageArea from
|
|
50
|
-
import alertHandler from
|
|
51
|
-
import { isFileLink, isImageEmbed } from
|
|
52
|
-
import { countShouldIgnore } from
|
|
53
|
-
import launchWordcountModal from
|
|
54
|
-
import { determineOSDependentKey } from
|
|
55
|
-
import skinCSS from
|
|
56
|
-
import contentCSS from
|
|
57
|
-
import { rceWrapperPropTypes } from
|
|
58
|
-
import { insertPlaceholder, placeholderInfoFor, removePlaceholder } from
|
|
59
|
-
import { transformRceContentForEditing } from
|
|
20
|
+
import React, { Suspense } from 'react';
|
|
21
|
+
import { Editor } from '@tinymce/tinymce-react';
|
|
22
|
+
import tinymce from 'tinymce';
|
|
23
|
+
import _ from 'lodash';
|
|
24
|
+
import { StoreProvider } from './plugins/shared/StoreContext';
|
|
25
|
+
import { IconKeyboardShortcutsLine } from '@instructure/ui-icons';
|
|
26
|
+
import { Alert } from '@instructure/ui-alerts';
|
|
27
|
+
import { Spinner } from '@instructure/ui-spinner';
|
|
28
|
+
import { View } from '@instructure/ui-view';
|
|
29
|
+
import { debounce } from '@instructure/debounce';
|
|
30
|
+
import { uid } from '@instructure/uid';
|
|
31
|
+
import { FocusRegionManager } from '@instructure/ui-a11y-utils';
|
|
32
|
+
import getCookie from '../common/getCookie';
|
|
33
|
+
import formatMessage from '../format-message';
|
|
34
|
+
import * as contentInsertion from './contentInsertion';
|
|
35
|
+
import indicatorRegion from './indicatorRegion';
|
|
36
|
+
import { editorLanguage } from './editorLanguage';
|
|
37
|
+
import normalizeLocale from './normalizeLocale';
|
|
38
|
+
import { sanitizePlugins } from './sanitizePlugins';
|
|
39
|
+
import RCEGlobals from './RCEGlobals';
|
|
40
|
+
import defaultTinymceConfig from '../defaultTinymceConfig';
|
|
41
|
+
import { FS_CHANGEEVENT, FS_ELEMENT, FS_ENABLED, FS_EXIT, FS_REQUEST, instuiPopupMountNodeFn } from '../util/fullscreenHelpers';
|
|
42
|
+
import indicate from '../common/indicate';
|
|
43
|
+
import bridge from '../bridge';
|
|
44
|
+
import CanvasContentTray from './plugins/shared/CanvasContentTray';
|
|
45
|
+
import StatusBar, { PRETTY_HTML_EDITOR_VIEW, RAW_HTML_EDITOR_VIEW, WYSIWYG_VIEW } from './StatusBar';
|
|
46
|
+
import { VIEW_CHANGE } from './customEvents';
|
|
47
|
+
import ShowOnFocusButton from './ShowOnFocusButton';
|
|
48
|
+
import KeyboardShortcutModal from './KeyboardShortcutModal';
|
|
49
|
+
import AlertMessageArea from './AlertMessageArea';
|
|
50
|
+
import alertHandler from './alertHandler';
|
|
51
|
+
import { isFileLink, isImageEmbed } from './plugins/shared/ContentSelection';
|
|
52
|
+
import { countShouldIgnore } from './plugins/instructure_wordcount/utils/countContent';
|
|
53
|
+
import launchWordcountModal from './plugins/instructure_wordcount/clickCallback';
|
|
54
|
+
import { determineOSDependentKey } from './userOS';
|
|
55
|
+
import skinCSS from './tinymce.oxide.skin.min.css';
|
|
56
|
+
import contentCSS from './tinymce.oxide.content.min.css';
|
|
57
|
+
import { rceWrapperPropTypes } from './RCEWrapperProps';
|
|
58
|
+
import { insertPlaceholder, placeholderInfoFor, removePlaceholder } from '../util/loadingPlaceholder';
|
|
59
|
+
import { transformRceContentForEditing } from './transformContent';
|
|
60
60
|
// @ts-expect-error
|
|
61
|
-
import { IconMoreSolid } from
|
|
62
|
-
import EncryptedStorage from
|
|
63
|
-
import buildStyle from
|
|
64
|
-
import { getMenubarForVariant, getMenuForVariant, getToolbarForVariant, getStatusBarFeaturesForVariant } from
|
|
65
|
-
import { focusFirstMenuButton, focusToolbar, isElementWithinTable, mergeMenu, mergeMenuItems, mergePlugins, mergeToolbar, parsePluginsToExclude, patchAutosavedContent } from
|
|
66
|
-
import { externalToolsForToolbar } from
|
|
67
|
-
import { initScreenreaderOnFormat } from
|
|
68
|
-
import { normalizeContainingContext } from
|
|
69
|
-
const RestoreAutoSaveModal = /*#__PURE__*/React.lazy(() => import(
|
|
70
|
-
const RceHtmlEditor = /*#__PURE__*/React.lazy(() => import(
|
|
61
|
+
import { IconMoreSolid } from '@instructure/ui-icons/es/svg';
|
|
62
|
+
import EncryptedStorage from '../util/encrypted-storage';
|
|
63
|
+
import buildStyle from './style';
|
|
64
|
+
import { getMenubarForVariant, getMenuForVariant, getToolbarForVariant, getStatusBarFeaturesForVariant } from './RCEVariants';
|
|
65
|
+
import { focusFirstMenuButton, focusToolbar, isElementWithinTable, mergeMenu, mergeMenuItems, mergePlugins, mergeToolbar, parsePluginsToExclude, patchAutosavedContent } from './RCEWrapper.utils';
|
|
66
|
+
import { externalToolsForToolbar } from './plugins/instructure_rce_external_tools/util/externalToolsForToolbar';
|
|
67
|
+
import { initScreenreaderOnFormat } from './screenreaderOnFormat';
|
|
68
|
+
import { normalizeContainingContext } from '../util/contextHelper';
|
|
69
|
+
const RestoreAutoSaveModal = /*#__PURE__*/React.lazy(() => import('./RestoreAutoSaveModal'));
|
|
70
|
+
const RceHtmlEditor = /*#__PURE__*/React.lazy(() => import('./RceHtmlEditor'));
|
|
71
71
|
const ASYNC_FOCUS_TIMEOUT = 250;
|
|
72
|
-
const DEFAULT_RCE_HEIGHT =
|
|
72
|
+
const DEFAULT_RCE_HEIGHT = '400px';
|
|
73
73
|
function addKebabIcon(editor) {
|
|
74
74
|
// This has to be done here instead of of in plugins/instructure-ui-icons/plugin.ts
|
|
75
75
|
// presumably because the toolbar gets created before that plugin is loaded?
|
|
76
|
-
editor.ui.registry.addIcon(
|
|
76
|
+
editor.ui.registry.addIcon('more-drawer', IconMoreSolid.src);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Get oxide the default skin injected into the DOM before the overrides loaded by themeable
|
|
@@ -81,8 +81,8 @@ let inserted = false;
|
|
|
81
81
|
function injectTinySkin() {
|
|
82
82
|
if (inserted) return;
|
|
83
83
|
inserted = true;
|
|
84
|
-
const style = document.createElement(
|
|
85
|
-
style.setAttribute(
|
|
84
|
+
const style = document.createElement('style');
|
|
85
|
+
style.setAttribute('data-skin', 'tiny oxide skin');
|
|
86
86
|
style.appendChild(document.createTextNode(skinCSS));
|
|
87
87
|
// there's CSS from discussions that turns the instui Selectors bold
|
|
88
88
|
// and in classic quizzes that also mucks with padding
|
|
@@ -90,9 +90,9 @@ function injectTinySkin() {
|
|
|
90
90
|
#discussion-edit-view .rce-wrapper input[readonly] {font-weight: normal;}
|
|
91
91
|
#quiz_edit_wrapper .rce-wrapper input[readonly] {font-weight: normal; padding-left: .75rem;}
|
|
92
92
|
`));
|
|
93
|
-
const beforeMe = document.head.querySelector(
|
|
93
|
+
const beforeMe = document.head.querySelector('style[data-glamor]') ||
|
|
94
94
|
// find instui's themeable stylesheet
|
|
95
|
-
document.head.querySelector(
|
|
95
|
+
document.head.querySelector('style') ||
|
|
96
96
|
// find any stylesheet
|
|
97
97
|
document.head.firstElementChild;
|
|
98
98
|
document.head.insertBefore(style, beforeMe);
|
|
@@ -105,7 +105,7 @@ export function storageAvailable() {
|
|
|
105
105
|
let storage = null;
|
|
106
106
|
try {
|
|
107
107
|
storage = window.localStorage;
|
|
108
|
-
const x =
|
|
108
|
+
const x = '__storage_test__';
|
|
109
109
|
storage.setItem(x, x);
|
|
110
110
|
storage.removeItem(x);
|
|
111
111
|
return true;
|
|
@@ -117,15 +117,15 @@ export function storageAvailable() {
|
|
|
117
117
|
e.code === 1014 ||
|
|
118
118
|
// test name field too, because code might not be present
|
|
119
119
|
// everything except Firefox
|
|
120
|
-
e.name ===
|
|
120
|
+
e.name === 'QuotaExceededError' ||
|
|
121
121
|
// Firefox
|
|
122
|
-
e.name ===
|
|
122
|
+
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
|
|
123
123
|
// acknowledge QuotaExceededError only if there's something already stored
|
|
124
124
|
storage && storage.length !== 0;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
function renderLoading() {
|
|
128
|
-
return formatMessage(
|
|
128
|
+
return formatMessage('Loading');
|
|
129
129
|
}
|
|
130
130
|
let alertIdValue = 0;
|
|
131
131
|
class RCEWrapper extends React.Component {
|
|
@@ -199,7 +199,7 @@ class RCEWrapper extends React.Component {
|
|
|
199
199
|
this.setState(newState);
|
|
200
200
|
this.checkAccessibility();
|
|
201
201
|
if (newView === PRETTY_HTML_EDITOR_VIEW || newView === RAW_HTML_EDITOR_VIEW) {
|
|
202
|
-
this.storage?.setItem?.(
|
|
202
|
+
this.storage?.setItem?.('rce.htmleditor', newView);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// Emit view change event
|
|
@@ -216,7 +216,7 @@ class RCEWrapper extends React.Component {
|
|
|
216
216
|
if (document[FS_ELEMENT]) {
|
|
217
217
|
// @ts-expect-error
|
|
218
218
|
this.resizeObserver.observe(document[FS_ELEMENT]);
|
|
219
|
-
window.visualViewport?.addEventListener(
|
|
219
|
+
window.visualViewport?.addEventListener('resize', this._handleFullscreenResize);
|
|
220
220
|
this._handleFullscreenResize();
|
|
221
221
|
// @ts-expect-error
|
|
222
222
|
this._focusRegion = FocusRegionManager.activateRegion(
|
|
@@ -227,7 +227,7 @@ class RCEWrapper extends React.Component {
|
|
|
227
227
|
} else {
|
|
228
228
|
event.target.removeEventListener(FS_CHANGEEVENT, this._onFullscreenChange);
|
|
229
229
|
this.resizeObserver.unobserve(event.target);
|
|
230
|
-
window.visualViewport?.removeEventListener(
|
|
230
|
+
window.visualViewport?.removeEventListener('resize', this._handleFullscreenResize);
|
|
231
231
|
this._setHeight(this.state.fullscreenState.prevHeight);
|
|
232
232
|
if (this._focusRegion) {
|
|
233
233
|
FocusRegionManager.blurRegion(event.target, this._focusRegion.id);
|
|
@@ -260,35 +260,35 @@ class RCEWrapper extends React.Component {
|
|
|
260
260
|
// what we've got for now.
|
|
261
261
|
const ifr = this.iframe;
|
|
262
262
|
if (ifr?.parentElement) {
|
|
263
|
-
ifr.parentElement.classList.add(
|
|
263
|
+
ifr.parentElement.classList.add('active');
|
|
264
264
|
}
|
|
265
265
|
this.handleFocus();
|
|
266
266
|
};
|
|
267
267
|
this.handleBlurEditor = event => {
|
|
268
268
|
const ifr = this.iframe;
|
|
269
269
|
if (ifr?.parentElement) {
|
|
270
|
-
ifr.parentElement.classList.remove(
|
|
270
|
+
ifr.parentElement.classList.remove('active');
|
|
271
271
|
}
|
|
272
272
|
this.handleBlur(event);
|
|
273
273
|
};
|
|
274
274
|
this.handleKey = event => {
|
|
275
|
-
if (event.code ===
|
|
275
|
+
if (event.code === 'F9' && event.altKey) {
|
|
276
276
|
event.preventDefault();
|
|
277
277
|
event.stopPropagation();
|
|
278
278
|
// @ts-expect-error
|
|
279
279
|
focusFirstMenuButton(this._elementRef.current);
|
|
280
|
-
} else if (event.code ===
|
|
280
|
+
} else if (event.code === 'F10' && event.altKey) {
|
|
281
281
|
event.preventDefault();
|
|
282
282
|
event.stopPropagation();
|
|
283
283
|
// @ts-expect-error
|
|
284
284
|
focusToolbar(this._elementRef.current);
|
|
285
|
-
} else if (event.code ===
|
|
285
|
+
} else if (event.code === 'F8' && event.altKey) {
|
|
286
286
|
event.preventDefault();
|
|
287
287
|
event.stopPropagation();
|
|
288
288
|
this.openKBShortcutModal();
|
|
289
|
-
} else if (event.code ===
|
|
289
|
+
} else if (event.code === 'Escape') {
|
|
290
290
|
bridge.hideTrays();
|
|
291
|
-
} else if ([
|
|
291
|
+
} else if (['n', 'N', 'd', 'D'].indexOf(event.key) !== -1) {
|
|
292
292
|
// Prevent key events from bubbling up on touch screen device
|
|
293
293
|
event.stopPropagation();
|
|
294
294
|
}
|
|
@@ -317,32 +317,57 @@ class RCEWrapper extends React.Component {
|
|
|
317
317
|
// @ts-expect-error
|
|
318
318
|
textarea.value = this.getCode();
|
|
319
319
|
textarea.style.height = this.state.height;
|
|
320
|
-
textarea.removeAttribute(
|
|
321
|
-
if (document.body.classList.contains(
|
|
320
|
+
textarea.removeAttribute('aria-hidden');
|
|
321
|
+
if (document.body.classList.contains('Underline-All-Links__enabled')) {
|
|
322
322
|
if (this.iframe?.contentDocument) {
|
|
323
|
-
this.iframe.contentDocument.body.classList.add(
|
|
323
|
+
this.iframe.contentDocument.body.classList.add('Underline-All-Links__enabled');
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
-
editor.on(
|
|
326
|
+
editor.on('wordCountUpdate', this.onWordCountUpdate);
|
|
327
327
|
// add an aria-label to the application div that wraps RCE
|
|
328
328
|
// and change role from "application" to "document" to ensure
|
|
329
329
|
// the editor gets properly picked up by screen readers
|
|
330
330
|
const tinyapp = document.querySelector('.tox-tinymce[role="application"]');
|
|
331
331
|
if (tinyapp) {
|
|
332
|
-
tinyapp.setAttribute(
|
|
333
|
-
tinyapp.setAttribute(
|
|
334
|
-
tinyapp.setAttribute(
|
|
332
|
+
tinyapp.setAttribute('aria-label', formatMessage('Rich Content Editor'));
|
|
333
|
+
tinyapp.setAttribute('role', 'document');
|
|
334
|
+
tinyapp.setAttribute('tabIndex', '-1');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// remove role="aplication" attribute from the iframe body
|
|
338
|
+
// tinymce adds this when the editor is wrapped in an iframe
|
|
339
|
+
// which makes RCE input fields inaccessible to screen readers
|
|
340
|
+
const iframe = tinyapp?.querySelector('iframe');
|
|
341
|
+
const body = iframe?.contentDocument?.body;
|
|
342
|
+
if (body) {
|
|
343
|
+
const observer = new MutationObserver(() => {
|
|
344
|
+
try {
|
|
345
|
+
if (body && body.getAttribute('role') === 'application') {
|
|
346
|
+
body.removeAttribute('role');
|
|
347
|
+
}
|
|
348
|
+
} catch (_) {
|
|
349
|
+
/* pass */
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
observer.observe(body, {
|
|
353
|
+
attributes: true,
|
|
354
|
+
childList: false,
|
|
355
|
+
subtree: false
|
|
356
|
+
});
|
|
357
|
+
body.setAttribute('data-role-checked', 'true'); // to trigger observer
|
|
358
|
+
|
|
359
|
+
setTimeout(() => observer.disconnect(), 10000);
|
|
335
360
|
}
|
|
336
361
|
|
|
337
362
|
// Probably should do this in tinymce.scss, but we only want it in new rce
|
|
338
|
-
textarea.style.resize =
|
|
339
|
-
editor.on(
|
|
340
|
-
editor.on(
|
|
363
|
+
textarea.style.resize = 'none';
|
|
364
|
+
editor.on('keydown', this.handleKey);
|
|
365
|
+
editor.on('FullscreenStateChanged', this._onFullscreenChange);
|
|
341
366
|
// This propagates click events on the editor out of the iframe to the parent
|
|
342
367
|
// document. We need this so that click events get captured properly by instui
|
|
343
368
|
// focus-trapping components, so they properly ignore trapping focus on click.
|
|
344
|
-
editor.on(
|
|
345
|
-
editor.on(
|
|
369
|
+
editor.on('click', () => window.document.body.click(), true);
|
|
370
|
+
editor.on('Cut Change input Undo Redo', debounce(this.handleInputChange, 1000));
|
|
346
371
|
initScreenreaderOnFormat(editor);
|
|
347
372
|
this.announceContextToolbars(editor);
|
|
348
373
|
if (this.isAutoSaving) {
|
|
@@ -354,12 +379,12 @@ class RCEWrapper extends React.Component {
|
|
|
354
379
|
|
|
355
380
|
// readonly should have been handled via the init property passed
|
|
356
381
|
// to <Editor>, but it's not.
|
|
357
|
-
editor.mode.set(this.props.readOnly ?
|
|
382
|
+
editor.mode.set(this.props.readOnly ? 'readonly' : 'design');
|
|
358
383
|
|
|
359
384
|
// Not using iframe_aria_text because compatibility issues.
|
|
360
385
|
// Not using iframe_attrs because library overwriting.
|
|
361
386
|
if (this.iframe) {
|
|
362
|
-
this.iframe.setAttribute(
|
|
387
|
+
this.iframe.setAttribute('title', formatMessage('Rich Text Area. Press {OSKey}+F8 for Rich Content Editor shortcuts.', {
|
|
363
388
|
OSKey: determineOSDependentKey()
|
|
364
389
|
}));
|
|
365
390
|
}
|
|
@@ -372,8 +397,8 @@ class RCEWrapper extends React.Component {
|
|
|
372
397
|
|
|
373
398
|
// cleans up highlight artifacts from findreplace plugin
|
|
374
399
|
if (this.getRequiredFeatureStatuses().rce_find_replace) {
|
|
375
|
-
editor.on(
|
|
376
|
-
if (editor?.dom?.doc?.getElementsByClassName?.(
|
|
400
|
+
editor.on('undo redo', _e => {
|
|
401
|
+
if (editor?.dom?.doc?.getElementsByClassName?.('mce-match-marker')?.length > 0) {
|
|
377
402
|
editor.plugins?.searchreplace?.done();
|
|
378
403
|
}
|
|
379
404
|
});
|
|
@@ -392,7 +417,7 @@ class RCEWrapper extends React.Component {
|
|
|
392
417
|
// This workaround removes attribute, thusly causing navigation to work correctly again.
|
|
393
418
|
// For the correct solution, Keying.config should have { selector: '.tox-toolbar__group' }
|
|
394
419
|
// in https://github.com/tinymce/tinymce/blob/develop/modules/alloy/src/main/ts/ephox/alloy/ui/schema/SplitSlidingToolbarSchema.ts
|
|
395
|
-
this._elementRef.current?.querySelectorAll(
|
|
420
|
+
this._elementRef.current?.querySelectorAll('.tox-toolbar-overlord button[data-alloy-tabstop]').forEach(it => it.removeAttribute('data-alloy-tabstop'));
|
|
396
421
|
};
|
|
397
422
|
/**
|
|
398
423
|
* Sets up selection saving and restoration logic.
|
|
@@ -419,7 +444,7 @@ class RCEWrapper extends React.Component {
|
|
|
419
444
|
selectionWasReset = false;
|
|
420
445
|
}
|
|
421
446
|
};
|
|
422
|
-
editor.on(
|
|
447
|
+
editor.on('blur', () => {
|
|
423
448
|
editorHasFocus = false;
|
|
424
449
|
selectionWasReset = false;
|
|
425
450
|
if (!this.editor) return;
|
|
@@ -428,7 +453,7 @@ class RCEWrapper extends React.Component {
|
|
|
428
453
|
isForward: this.editor.selection.isForward()
|
|
429
454
|
};
|
|
430
455
|
});
|
|
431
|
-
editor.on(
|
|
456
|
+
editor.on('focus', () => {
|
|
432
457
|
// We need to restore the selection when the editor regains focus because sometimes the editor regains
|
|
433
458
|
// focus without the user setting the selection themselves (such as when they interact with the toolbar)
|
|
434
459
|
// and if we didn't, we would end up saving the reset selection before a user managed to actually insert
|
|
@@ -437,7 +462,7 @@ class RCEWrapper extends React.Component {
|
|
|
437
462
|
editorHasFocus = true;
|
|
438
463
|
selectionWasReset = false;
|
|
439
464
|
});
|
|
440
|
-
editor.on(
|
|
465
|
+
editor.on('SelectionChange', () => {
|
|
441
466
|
if (editorHasFocus) {
|
|
442
467
|
// We don't care if a selection reset occurs when the editor has focus, the user probably intended that
|
|
443
468
|
// At least they will see the effect
|
|
@@ -447,14 +472,14 @@ class RCEWrapper extends React.Component {
|
|
|
447
472
|
const selection = this.editor.selection.normalize();
|
|
448
473
|
|
|
449
474
|
// Detect a browser-reset selection (e.g. From invoking the Find command)
|
|
450
|
-
if (selection.startContainer?.nodeName ===
|
|
475
|
+
if (selection.startContainer?.nodeName === 'BODY' && selection.startContainer === selection.endContainer && selection.startOffset === 0 && selection.endOffset === 0) {
|
|
451
476
|
selectionWasReset = true;
|
|
452
477
|
}
|
|
453
478
|
});
|
|
454
|
-
editor.on(
|
|
479
|
+
editor.on('BeforeExecCommand', () => {
|
|
455
480
|
restoreSelectionIfNecessary();
|
|
456
481
|
});
|
|
457
|
-
editor.on(
|
|
482
|
+
editor.on('ExecCommand', (/* event */
|
|
458
483
|
) => {
|
|
459
484
|
if (!this.editor) return;
|
|
460
485
|
// Commands may have modified the selection, we need to recapture it
|
|
@@ -469,10 +494,10 @@ class RCEWrapper extends React.Component {
|
|
|
469
494
|
/* ********** autosave support *************** */
|
|
470
495
|
this.initAutoSave = editor => {
|
|
471
496
|
var _this$props$userCache;
|
|
472
|
-
this.storage = new EncryptedStorage((_this$props$userCache = this.props.userCacheKey) !== null && _this$props$userCache !== void 0 ? _this$props$userCache :
|
|
497
|
+
this.storage = new EncryptedStorage((_this$props$userCache = this.props.userCacheKey) !== null && _this$props$userCache !== void 0 ? _this$props$userCache : '');
|
|
473
498
|
if (this.storage) {
|
|
474
|
-
editor.on(
|
|
475
|
-
editor.on(
|
|
499
|
+
editor.on('change Undo Redo', this.doAutoSave);
|
|
500
|
+
editor.on('blur', this.doAutoSave);
|
|
476
501
|
this.cleanupAutoSave();
|
|
477
502
|
try {
|
|
478
503
|
const autosaved = this.getAutoSaved(this.autoSaveKey);
|
|
@@ -497,7 +522,7 @@ class RCEWrapper extends React.Component {
|
|
|
497
522
|
} catch (ex) {
|
|
498
523
|
// log and ignore
|
|
499
524
|
|
|
500
|
-
console.error(
|
|
525
|
+
console.error('Failed initializing rce autosave', ex);
|
|
501
526
|
}
|
|
502
527
|
}
|
|
503
528
|
};
|
|
@@ -552,7 +577,7 @@ class RCEWrapper extends React.Component {
|
|
|
552
577
|
this.cleanupAutoSave(true);
|
|
553
578
|
this.doAutoSave(e, true);
|
|
554
579
|
} else {
|
|
555
|
-
console.error(
|
|
580
|
+
console.error('Autosave failed:', ex);
|
|
556
581
|
}
|
|
557
582
|
}
|
|
558
583
|
}
|
|
@@ -560,7 +585,7 @@ class RCEWrapper extends React.Component {
|
|
|
560
585
|
/* *********** end autosave support *************** */
|
|
561
586
|
this.onWordCountUpdate = e => {
|
|
562
587
|
if (!this.editor) return;
|
|
563
|
-
const shouldIgnore = countShouldIgnore(this.editor,
|
|
588
|
+
const shouldIgnore = countShouldIgnore(this.editor, 'body', 'words');
|
|
564
589
|
const updatedCount = e.wordCount.words - shouldIgnore;
|
|
565
590
|
this.setState(state => {
|
|
566
591
|
if (updatedCount !== state.wordCount) {
|
|
@@ -573,7 +598,7 @@ class RCEWrapper extends React.Component {
|
|
|
573
598
|
// @ts-expect-error
|
|
574
599
|
this.onNodeChange = e => {
|
|
575
600
|
// This is basically copied out of the tinymce silver theme code for the status bar
|
|
576
|
-
const path = e.parents.filter(p => p.nodeName !==
|
|
601
|
+
const path = e.parents.filter(p => p.nodeName !== 'BR' && !p.getAttribute('data-mce-bogus') && p.getAttribute('data-mce-type') !== 'bookmark')
|
|
577
602
|
// @ts-expect-error
|
|
578
603
|
.map(p => p.nodeName.toLowerCase()).reverse();
|
|
579
604
|
this.setState({
|
|
@@ -584,7 +609,7 @@ class RCEWrapper extends React.Component {
|
|
|
584
609
|
this.props.onContentChange?.(content);
|
|
585
610
|
// check accessibility when clearing the editor,
|
|
586
611
|
// all other times should be checked by handleInputChange
|
|
587
|
-
if (content ===
|
|
612
|
+
if (content === '') {
|
|
588
613
|
this.checkAccessibility();
|
|
589
614
|
}
|
|
590
615
|
};
|
|
@@ -606,14 +631,14 @@ class RCEWrapper extends React.Component {
|
|
|
606
631
|
height: newHeight
|
|
607
632
|
});
|
|
608
633
|
// play nice and send the same event that the silver theme would send
|
|
609
|
-
editor.fire(
|
|
634
|
+
editor.fire('ResizeEditor', {
|
|
610
635
|
deltaY: coordinates.deltaY
|
|
611
636
|
});
|
|
612
637
|
}
|
|
613
638
|
};
|
|
614
639
|
this.onA11yChecker = triggerElementId => {
|
|
615
640
|
const editor = this.mceInstance();
|
|
616
|
-
editor.execCommand(
|
|
641
|
+
editor.execCommand('openAccessibilityChecker', false, {
|
|
617
642
|
mountNode: instuiPopupMountNodeFn,
|
|
618
643
|
triggerElementId,
|
|
619
644
|
onFixError: errors => {
|
|
@@ -627,7 +652,7 @@ class RCEWrapper extends React.Component {
|
|
|
627
652
|
};
|
|
628
653
|
this.checkAccessibility = () => {
|
|
629
654
|
const editor = this.mceInstance();
|
|
630
|
-
editor.execCommand(
|
|
655
|
+
editor.execCommand('checkAccessibility', false, {
|
|
631
656
|
// @ts-expect-error
|
|
632
657
|
done: errors => {
|
|
633
658
|
this.setState({
|
|
@@ -667,7 +692,7 @@ class RCEWrapper extends React.Component {
|
|
|
667
692
|
}
|
|
668
693
|
};
|
|
669
694
|
this.handleAIClick = () => {
|
|
670
|
-
import(
|
|
695
|
+
import('./plugins/shared/ai_tools').then(module => {
|
|
671
696
|
// @ts-expect-error
|
|
672
697
|
this.AIToolsTray = module.AIToolsTray;
|
|
673
698
|
this.setState({
|
|
@@ -675,7 +700,7 @@ class RCEWrapper extends React.Component {
|
|
|
675
700
|
AITToolsFocusReturn: document.activeElement
|
|
676
701
|
});
|
|
677
702
|
}).catch(ex => {
|
|
678
|
-
console.error(
|
|
703
|
+
console.error('Failed loading the AIToolsTray', ex);
|
|
679
704
|
});
|
|
680
705
|
};
|
|
681
706
|
this.closeAITools = () => {
|
|
@@ -717,10 +742,10 @@ class RCEWrapper extends React.Component {
|
|
|
717
742
|
this.getCurrentContentForAI = () => {
|
|
718
743
|
const selected = this.mceInstance().selection.getContent();
|
|
719
744
|
return selected ? {
|
|
720
|
-
type:
|
|
745
|
+
type: 'selection',
|
|
721
746
|
content: selected
|
|
722
747
|
} : {
|
|
723
|
-
type:
|
|
748
|
+
type: 'full',
|
|
724
749
|
content: this.mceInstance().getContent()
|
|
725
750
|
};
|
|
726
751
|
};
|
|
@@ -735,7 +760,7 @@ class RCEWrapper extends React.Component {
|
|
|
735
760
|
alert.id = alertIdValue++;
|
|
736
761
|
this.setState(state => {
|
|
737
762
|
let messages = state.messages.concat(alert);
|
|
738
|
-
messages = _.uniqBy(messages,
|
|
763
|
+
messages = _.uniqBy(messages, 'text'); // Don't show the same message twice
|
|
739
764
|
return {
|
|
740
765
|
messages
|
|
741
766
|
};
|
|
@@ -754,7 +779,7 @@ class RCEWrapper extends React.Component {
|
|
|
754
779
|
*/
|
|
755
780
|
this.resetAlertId = () => {
|
|
756
781
|
if (this.state.messages.length > 0) {
|
|
757
|
-
throw new Error(
|
|
782
|
+
throw new Error('There are messages currently, you cannot reset when they are non-zero');
|
|
758
783
|
}
|
|
759
784
|
alertIdValue = 0;
|
|
760
785
|
};
|
|
@@ -796,7 +821,7 @@ class RCEWrapper extends React.Component {
|
|
|
796
821
|
if (!Number.isNaN(_ht)) {
|
|
797
822
|
_ht = `${_ht}px`;
|
|
798
823
|
}
|
|
799
|
-
const currentRCECount = document.querySelectorAll(
|
|
824
|
+
const currentRCECount = document.querySelectorAll('.rce-wrapper').length;
|
|
800
825
|
const maxInitRenderedRCEs = Number.isNaN(props.maxInitRenderedRCEs) ? RCEWrapper.defaultProps.maxInitRenderedRCEs : props.maxInitRenderedRCEs;
|
|
801
826
|
this.state = {
|
|
802
827
|
path: [],
|
|
@@ -807,9 +832,9 @@ class RCEWrapper extends React.Component {
|
|
|
807
832
|
messages: [],
|
|
808
833
|
announcement: null,
|
|
809
834
|
confirmAutoSave: false,
|
|
810
|
-
autoSavedContent:
|
|
835
|
+
autoSavedContent: '',
|
|
811
836
|
// @ts-expect-error
|
|
812
|
-
id: this.props.id || this.props.textareaId || `${uid(
|
|
837
|
+
id: this.props.id || this.props.textareaId || `${uid('rce', 2)}`,
|
|
813
838
|
// @ts-expect-error
|
|
814
839
|
height: _ht,
|
|
815
840
|
fullscreenState: {
|
|
@@ -817,7 +842,7 @@ class RCEWrapper extends React.Component {
|
|
|
817
842
|
prevHeight: _ht
|
|
818
843
|
},
|
|
819
844
|
a11yErrorsCount: 0,
|
|
820
|
-
shouldShowEditor: typeof IntersectionObserver ===
|
|
845
|
+
shouldShowEditor: typeof IntersectionObserver === 'undefined' || maxInitRenderedRCEs <= 0 || currentRCECount < maxInitRenderedRCEs,
|
|
821
846
|
AIToolsOpen: false
|
|
822
847
|
};
|
|
823
848
|
this._statusBarId = `${this.state.id}_statusbar`;
|
|
@@ -852,11 +877,11 @@ class RCEWrapper extends React.Component {
|
|
|
852
877
|
// configure tinymce to say where that div is mounted, do this
|
|
853
878
|
// is a bit of a hack to tag the div that is this RCE's
|
|
854
879
|
_tagTinymceAuxDiv() {
|
|
855
|
-
const tinyauxlist = document.querySelectorAll(
|
|
880
|
+
const tinyauxlist = document.querySelectorAll('.tox-tinymce-aux');
|
|
856
881
|
if (tinyauxlist.length) {
|
|
857
882
|
const myaux = tinyauxlist[tinyauxlist.length - 1];
|
|
858
883
|
if (myaux.id) {
|
|
859
|
-
console.error(
|
|
884
|
+
console.error('Unexpected ID on my tox-tinymce-aux element');
|
|
860
885
|
}
|
|
861
886
|
myaux.id = `tinyaux-${this.id}`;
|
|
862
887
|
}
|
|
@@ -916,7 +941,7 @@ class RCEWrapper extends React.Component {
|
|
|
916
941
|
let status = true;
|
|
917
942
|
// Check for remaining placeholders
|
|
918
943
|
if (this.mceInstance().dom.doc.querySelector(`[data-placeholder-for]`)) {
|
|
919
|
-
status = promptFunc(formatMessage(
|
|
944
|
+
status = promptFunc(formatMessage('Content is still being uploaded, if you continue it will not be embedded properly.'));
|
|
920
945
|
}
|
|
921
946
|
return status;
|
|
922
947
|
}
|
|
@@ -976,9 +1001,9 @@ class RCEWrapper extends React.Component {
|
|
|
976
1001
|
// @ts-expect-error
|
|
977
1002
|
ifr.contentDocument.body.clientHeight -
|
|
978
1003
|
// @ts-expect-error
|
|
979
|
-
parseInt(editor_body_style[
|
|
1004
|
+
parseInt(editor_body_style['padding-top'], 10) -
|
|
980
1005
|
// @ts-expect-error
|
|
981
|
-
parseInt(editor_body_style[
|
|
1006
|
+
parseInt(editor_body_style['padding-bottom'], 10);
|
|
982
1007
|
const para_margin_ht = 24;
|
|
983
1008
|
const reserve_ht = Math.ceil(height + para_margin_ht);
|
|
984
1009
|
if (reserve_ht > editor_ht) {
|
|
@@ -990,7 +1015,7 @@ class RCEWrapper extends React.Component {
|
|
|
990
1015
|
}
|
|
991
1016
|
}
|
|
992
1017
|
checkImageLoadError(element) {
|
|
993
|
-
if (!element || element.tagName !==
|
|
1018
|
+
if (!element || element.tagName !== 'IMG') {
|
|
994
1019
|
return;
|
|
995
1020
|
}
|
|
996
1021
|
// @ts-expect-error
|
|
@@ -1005,9 +1030,9 @@ class RCEWrapper extends React.Component {
|
|
|
1005
1030
|
// @ts-expect-error
|
|
1006
1031
|
if (element.naturalWidth === 0) {
|
|
1007
1032
|
// @ts-expect-error
|
|
1008
|
-
element.style.border =
|
|
1033
|
+
element.style.border = '1px solid #000';
|
|
1009
1034
|
// @ts-expect-error
|
|
1010
|
-
element.style.padding =
|
|
1035
|
+
element.style.padding = '2px';
|
|
1011
1036
|
}
|
|
1012
1037
|
}, 0);
|
|
1013
1038
|
}
|
|
@@ -1017,7 +1042,7 @@ class RCEWrapper extends React.Component {
|
|
|
1017
1042
|
this.contentInserted(element);
|
|
1018
1043
|
}
|
|
1019
1044
|
replaceCode(code) {
|
|
1020
|
-
if (code !==
|
|
1045
|
+
if (code !== '' && window.confirm(formatMessage('Content in the editor will be changed. Press Cancel to keep the original content.'))) {
|
|
1021
1046
|
this.mceInstance().setContent(code);
|
|
1022
1047
|
}
|
|
1023
1048
|
}
|
|
@@ -1034,12 +1059,12 @@ class RCEWrapper extends React.Component {
|
|
|
1034
1059
|
// that there's some embedded content helper
|
|
1035
1060
|
// From what I've read, "title" is more reliable than "aria-label" for
|
|
1036
1061
|
// elements like iframes and embeds.
|
|
1037
|
-
const temp = document.createElement(
|
|
1062
|
+
const temp = document.createElement('div');
|
|
1038
1063
|
temp.innerHTML = code;
|
|
1039
1064
|
const code_elem = temp.firstElementChild;
|
|
1040
1065
|
if (code_elem) {
|
|
1041
|
-
if (!code_elem.hasAttribute(
|
|
1042
|
-
code_elem.setAttribute(
|
|
1066
|
+
if (!code_elem.hasAttribute('title') && !code_elem.hasAttribute('aria-label')) {
|
|
1067
|
+
code_elem.setAttribute('title', formatMessage('embedded content'));
|
|
1043
1068
|
}
|
|
1044
1069
|
code = code_elem.outerHTML;
|
|
1045
1070
|
}
|
|
@@ -1049,7 +1074,7 @@ class RCEWrapper extends React.Component {
|
|
|
1049
1074
|
// and it's often inserted into a <p> on top of that. Find the
|
|
1050
1075
|
// iframe and use it to flash the indicator.
|
|
1051
1076
|
const element = contentInsertion.insertContent(editor, code);
|
|
1052
|
-
const ifr = element && element.querySelector && element.querySelector(
|
|
1077
|
+
const ifr = element && element.querySelector && element.querySelector('iframe');
|
|
1053
1078
|
if (ifr) {
|
|
1054
1079
|
this.contentInserted(ifr);
|
|
1055
1080
|
} else {
|
|
@@ -1061,7 +1086,7 @@ class RCEWrapper extends React.Component {
|
|
|
1061
1086
|
const element = contentInsertion.insertImage(editor, image, this.getCanvasUrl());
|
|
1062
1087
|
|
|
1063
1088
|
// Removes TinyMCE's caret text if exists.
|
|
1064
|
-
if (element?.nextSibling?.data?.startsWith(
|
|
1089
|
+
if (element?.nextSibling?.data?.startsWith('\xA0' /* nbsp */)) {
|
|
1065
1090
|
element.nextSibling.splitText(1);
|
|
1066
1091
|
element.nextSibling.remove();
|
|
1067
1092
|
}
|
|
@@ -1142,8 +1167,8 @@ class RCEWrapper extends React.Component {
|
|
|
1142
1167
|
onTinyMCEInstance(command, ...args) {
|
|
1143
1168
|
const editor = this.mceInstance();
|
|
1144
1169
|
if (editor) {
|
|
1145
|
-
if (command ===
|
|
1146
|
-
editor.execCommand(
|
|
1170
|
+
if (command === 'mceRemoveEditor') {
|
|
1171
|
+
editor.execCommand('mceNewDocument');
|
|
1147
1172
|
} // makes sure content can't persist past removal
|
|
1148
1173
|
editor.execCommand(command, false, ...args);
|
|
1149
1174
|
}
|
|
@@ -1163,17 +1188,17 @@ class RCEWrapper extends React.Component {
|
|
|
1163
1188
|
return null;
|
|
1164
1189
|
}
|
|
1165
1190
|
textareaValue() {
|
|
1166
|
-
return this.getTextarea()?.value ||
|
|
1191
|
+
return this.getTextarea()?.value || '';
|
|
1167
1192
|
}
|
|
1168
1193
|
get id() {
|
|
1169
1194
|
return this.state.id;
|
|
1170
1195
|
}
|
|
1171
1196
|
getHtmlEditorStorage() {
|
|
1172
|
-
const cookieValue = getCookie(
|
|
1197
|
+
const cookieValue = getCookie('rce.htmleditor');
|
|
1173
1198
|
if (cookieValue) {
|
|
1174
1199
|
document.cookie = `rce.htmleditor=${cookieValue};path=/;max-age=0`;
|
|
1175
1200
|
}
|
|
1176
|
-
const value = cookieValue || this.storage?.getItem?.(
|
|
1201
|
+
const value = cookieValue || this.storage?.getItem?.('rce.htmleditor')?.content;
|
|
1177
1202
|
return value === RAW_HTML_EDITOR_VIEW || value === PRETTY_HTML_EDITOR_VIEW ? value : PRETTY_HTML_EDITOR_VIEW;
|
|
1178
1203
|
}
|
|
1179
1204
|
_isFullscreen() {
|
|
@@ -1190,7 +1215,7 @@ class RCEWrapper extends React.Component {
|
|
|
1190
1215
|
this._elementRef.current?.appendChild(tinymenuhost);
|
|
1191
1216
|
}
|
|
1192
1217
|
this._elementRef.current?.addEventListener(FS_CHANGEEVENT, this._onFullscreenChange);
|
|
1193
|
-
if (typeof this._elementRef.current?.offsetHeight ===
|
|
1218
|
+
if (typeof this._elementRef.current?.offsetHeight === 'number') {
|
|
1194
1219
|
this.setState({
|
|
1195
1220
|
fullscreenState: {
|
|
1196
1221
|
prevHeight: this._elementRef.current.offsetHeight - this._getStatusBarHeight()
|
|
@@ -1222,7 +1247,7 @@ class RCEWrapper extends React.Component {
|
|
|
1222
1247
|
const container = ed.getContainer();
|
|
1223
1248
|
if (container) {
|
|
1224
1249
|
container.style.height = cssHeight;
|
|
1225
|
-
ed.fire(
|
|
1250
|
+
ed.fire('ResizeEditor');
|
|
1226
1251
|
}
|
|
1227
1252
|
const textarea = this.getTextarea();
|
|
1228
1253
|
if (textarea) {
|
|
@@ -1233,10 +1258,10 @@ class RCEWrapper extends React.Component {
|
|
|
1233
1258
|
});
|
|
1234
1259
|
}
|
|
1235
1260
|
focus() {
|
|
1236
|
-
this.onTinyMCEInstance(
|
|
1261
|
+
this.onTinyMCEInstance('mceFocus');
|
|
1237
1262
|
// tinymce doesn't always call the focus handler.
|
|
1238
1263
|
// @ts-expect-error
|
|
1239
|
-
this.handleFocusEditor(new Event(
|
|
1264
|
+
this.handleFocusEditor(new Event('focus', {
|
|
1240
1265
|
target: this.mceInstance()
|
|
1241
1266
|
}));
|
|
1242
1267
|
}
|
|
@@ -1274,7 +1299,7 @@ class RCEWrapper extends React.Component {
|
|
|
1274
1299
|
*/
|
|
1275
1300
|
get _mceSerializedInitialHtml() {
|
|
1276
1301
|
if (!this._mceSerializedInitialHtmlCached) {
|
|
1277
|
-
const el = window.document.createElement(
|
|
1302
|
+
const el = window.document.createElement('div');
|
|
1278
1303
|
// @ts-expect-error
|
|
1279
1304
|
el.innerHTML = this.initialContent;
|
|
1280
1305
|
const serializer = this.mceInstance().serializer;
|
|
@@ -1328,22 +1353,22 @@ class RCEWrapper extends React.Component {
|
|
|
1328
1353
|
// focus is still somewhere w/in me
|
|
1329
1354
|
return;
|
|
1330
1355
|
}
|
|
1331
|
-
const activeClass = document.activeElement?.getAttribute(
|
|
1356
|
+
const activeClass = document.activeElement?.getAttribute('class');
|
|
1332
1357
|
if (
|
|
1333
1358
|
// @ts-expect-error
|
|
1334
1359
|
(event.focusedEditor === undefined ||
|
|
1335
1360
|
// @ts-expect-error
|
|
1336
|
-
event.target.id === event.focusedEditor?.id) && activeClass?.includes(
|
|
1361
|
+
event.target.id === event.focusedEditor?.id) && activeClass?.includes('tox-')) {
|
|
1337
1362
|
// if a toolbar button has focus, then the user clicks on the "more" button
|
|
1338
1363
|
// focus jumps to the body, then eventually to the popped up toolbar. This
|
|
1339
1364
|
// catches that case.
|
|
1340
1365
|
return;
|
|
1341
1366
|
}
|
|
1342
|
-
if (event?.relatedTarget?.getAttribute(
|
|
1367
|
+
if (event?.relatedTarget?.getAttribute('class')?.includes('tox-')) {
|
|
1343
1368
|
// a tinymce popup has focus
|
|
1344
1369
|
return;
|
|
1345
1370
|
}
|
|
1346
|
-
const popups = document.querySelectorAll(
|
|
1371
|
+
const popups = document.querySelectorAll('[data-mce-component]');
|
|
1347
1372
|
for (const popup of popups) {
|
|
1348
1373
|
if (popup.contains(document.activeElement)) {
|
|
1349
1374
|
// one of our popups has focus
|
|
@@ -1361,22 +1386,22 @@ class RCEWrapper extends React.Component {
|
|
|
1361
1386
|
call(methodName, ...args) {
|
|
1362
1387
|
// since exists? has a ? and cant be a regular function just return true
|
|
1363
1388
|
// rather than calling as a fn on the editor
|
|
1364
|
-
if (methodName ===
|
|
1389
|
+
if (methodName === 'exists?') {
|
|
1365
1390
|
return true;
|
|
1366
1391
|
}
|
|
1367
1392
|
// @ts-expect-error
|
|
1368
1393
|
return this[methodName](...args);
|
|
1369
1394
|
}
|
|
1370
1395
|
announceContextToolbars(editor) {
|
|
1371
|
-
editor.on(
|
|
1396
|
+
editor.on('NodeChange', () => {
|
|
1372
1397
|
if (!this._isMounted) return;
|
|
1373
1398
|
const node = editor.selection.getNode();
|
|
1374
1399
|
// @ts-expect-error
|
|
1375
1400
|
if (isImageEmbed(node, editor)) {
|
|
1376
1401
|
if (this.announcing !== 1) {
|
|
1377
1402
|
this.setState({
|
|
1378
|
-
announcement: formatMessage(
|
|
1379
|
-
text: node.getAttribute(
|
|
1403
|
+
announcement: formatMessage('type Control F9 to access image options. {text}', {
|
|
1404
|
+
text: node.getAttribute('alt')
|
|
1380
1405
|
})
|
|
1381
1406
|
});
|
|
1382
1407
|
this.announcing = 1;
|
|
@@ -1384,7 +1409,7 @@ class RCEWrapper extends React.Component {
|
|
|
1384
1409
|
} else if (isFileLink(node, editor)) {
|
|
1385
1410
|
if (this.announcing !== 2) {
|
|
1386
1411
|
this.setState({
|
|
1387
|
-
announcement: formatMessage(
|
|
1412
|
+
announcement: formatMessage('type Control F9 to access link options. {text}', {
|
|
1388
1413
|
text: node.textContent
|
|
1389
1414
|
})
|
|
1390
1415
|
});
|
|
@@ -1393,7 +1418,7 @@ class RCEWrapper extends React.Component {
|
|
|
1393
1418
|
} else if (isElementWithinTable(node)) {
|
|
1394
1419
|
if (this.announcing !== 3) {
|
|
1395
1420
|
this.setState({
|
|
1396
|
-
announcement: formatMessage(
|
|
1421
|
+
announcement: formatMessage('type Control F9 to access table options. {text}', {
|
|
1397
1422
|
text: node.textContent
|
|
1398
1423
|
})
|
|
1399
1424
|
});
|
|
@@ -1406,17 +1431,17 @@ class RCEWrapper extends React.Component {
|
|
|
1406
1431
|
this.announcing = 0;
|
|
1407
1432
|
}
|
|
1408
1433
|
});
|
|
1409
|
-
editor.on(
|
|
1434
|
+
editor.on('ResizeEditor', ({
|
|
1410
1435
|
deltaY
|
|
1411
1436
|
}) => {
|
|
1412
1437
|
if (!this._isMounted || !deltaY) return;
|
|
1413
1438
|
if (deltaY < 0) {
|
|
1414
1439
|
this.setState({
|
|
1415
|
-
announcement: formatMessage(
|
|
1440
|
+
announcement: formatMessage('The height of Rich Content Area is decreased.')
|
|
1416
1441
|
});
|
|
1417
1442
|
} else {
|
|
1418
1443
|
this.setState({
|
|
1419
|
-
announcement: formatMessage(
|
|
1444
|
+
announcement: formatMessage('The height of Rich Content Area is increased.')
|
|
1420
1445
|
});
|
|
1421
1446
|
}
|
|
1422
1447
|
});
|
|
@@ -1442,10 +1467,10 @@ class RCEWrapper extends React.Component {
|
|
|
1442
1467
|
// This doesn't apply if the editor is off-screen or has visibility:hidden;
|
|
1443
1468
|
// only if it isn't rendered or has display:none;
|
|
1444
1469
|
const editorVisible = this.editor.getContainer().offsetParent;
|
|
1445
|
-
return this.props.autosave?.enabled && editorVisible && document.querySelectorAll(
|
|
1470
|
+
return this.props.autosave?.enabled && editorVisible && document.querySelectorAll('.rce-wrapper').length === 1 && storageAvailable();
|
|
1446
1471
|
}
|
|
1447
1472
|
get autoSaveKey() {
|
|
1448
|
-
const userId = this._effectiveContainingContext?.userId ||
|
|
1473
|
+
const userId = this._effectiveContainingContext?.userId || '-';
|
|
1449
1474
|
return `rceautosave:${userId}${window.location.href}:${this.props.textareaId}`;
|
|
1450
1475
|
}
|
|
1451
1476
|
componentWillUnmount() {
|
|
@@ -1456,7 +1481,7 @@ class RCEWrapper extends React.Component {
|
|
|
1456
1481
|
this.destroy();
|
|
1457
1482
|
}
|
|
1458
1483
|
if (this._elementRef.current) {
|
|
1459
|
-
this._elementRef.current.removeEventListener(
|
|
1484
|
+
this._elementRef.current.removeEventListener('keydown', this.handleKey, true);
|
|
1460
1485
|
}
|
|
1461
1486
|
this.mutationObserver?.disconnect();
|
|
1462
1487
|
this.intersectionObserver?.disconnect();
|
|
@@ -1468,27 +1493,27 @@ class RCEWrapper extends React.Component {
|
|
|
1468
1493
|
|
|
1469
1494
|
// @ts-expect-error
|
|
1470
1495
|
const setupCallback = options.setup;
|
|
1471
|
-
const canvasPlugins = rcsExists ? [
|
|
1496
|
+
const canvasPlugins = rcsExists ? ['instructure_image', 'instructure_documents', 'instructure_equation'] : [];
|
|
1472
1497
|
if (rcsExists && !this.props.instRecordDisabled) {
|
|
1473
|
-
canvasPlugins.splice(2, 0,
|
|
1498
|
+
canvasPlugins.splice(2, 0, 'instructure_record');
|
|
1474
1499
|
}
|
|
1475
|
-
const pastePlugins = rcsExists ? [
|
|
1476
|
-
if (rcsExists && this.props.use_rce_icon_maker && this.props.trayProps?.contextType ===
|
|
1477
|
-
canvasPlugins.push(
|
|
1500
|
+
const pastePlugins = rcsExists ? ['instructure_paste', 'paste'] : ['paste'];
|
|
1501
|
+
if (rcsExists && this.props.use_rce_icon_maker && this.props.trayProps?.contextType === 'course') {
|
|
1502
|
+
canvasPlugins.push('instructure_icon_maker');
|
|
1478
1503
|
}
|
|
1479
1504
|
if (document[FS_ENABLED]) {
|
|
1480
|
-
canvasPlugins.push(
|
|
1505
|
+
canvasPlugins.push('instructure_fullscreen');
|
|
1481
1506
|
}
|
|
1482
1507
|
if (this.getRequiredFeatureStatuses().rce_find_replace) {
|
|
1483
|
-
canvasPlugins.push(
|
|
1484
|
-
canvasPlugins.push(
|
|
1508
|
+
canvasPlugins.push('searchreplace');
|
|
1509
|
+
canvasPlugins.push('instructure_search_and_replace');
|
|
1485
1510
|
}
|
|
1486
|
-
const possibleNewMenubarItems = this.props.editorOptions.menu ? Object.keys(this.props.editorOptions.menu).join(
|
|
1511
|
+
const possibleNewMenubarItems = this.props.editorOptions.menu ? Object.keys(this.props.editorOptions.menu).join(' ') : undefined;
|
|
1487
1512
|
const wrappedOpts = {
|
|
1488
1513
|
...defaultTinymceConfig,
|
|
1489
1514
|
...options,
|
|
1490
1515
|
readonly: this.props.readOnly,
|
|
1491
|
-
theme:
|
|
1516
|
+
theme: 'silver',
|
|
1492
1517
|
// some older code specified 'modern', which doesn't exist any more
|
|
1493
1518
|
|
|
1494
1519
|
// @ts-expect-error
|
|
@@ -1497,7 +1522,7 @@ class RCEWrapper extends React.Component {
|
|
|
1497
1522
|
document_base_url: this.props.canvasOrigin,
|
|
1498
1523
|
block_formats:
|
|
1499
1524
|
// @ts-expect-error
|
|
1500
|
-
options.block_formats || [`${formatMessage(
|
|
1525
|
+
options.block_formats || [`${formatMessage('Heading 2')}=h2`, `${formatMessage('Heading 3')}=h3`, `${formatMessage('Heading 4')}=h4`, `${formatMessage('Preformatted')}=pre`, `${formatMessage('Paragraph')}=p`].join('; '),
|
|
1501
1526
|
setup: editor => {
|
|
1502
1527
|
addKebabIcon(editor);
|
|
1503
1528
|
editorWrappers.set(editor, this);
|
|
@@ -1510,7 +1535,7 @@ class RCEWrapper extends React.Component {
|
|
|
1510
1535
|
// @ts-expect-error
|
|
1511
1536
|
bridge.userLocale = userLocale;
|
|
1512
1537
|
bridge.canvasOrigin = this.props.canvasOrigin;
|
|
1513
|
-
if (typeof setupCallback ===
|
|
1538
|
+
if (typeof setupCallback === 'function') {
|
|
1514
1539
|
setupCallback(editor);
|
|
1515
1540
|
}
|
|
1516
1541
|
},
|
|
@@ -1521,7 +1546,7 @@ class RCEWrapper extends React.Component {
|
|
|
1521
1546
|
// @ts-expect-error
|
|
1522
1547
|
content_css: options.content_css || [],
|
|
1523
1548
|
// @ts-expect-error
|
|
1524
|
-
content_style: contentCSS + (options.content_style ||
|
|
1549
|
+
content_style: contentCSS + (options.content_style || ''),
|
|
1525
1550
|
menubar: mergeMenuItems(getMenubarForVariant(this.variant), possibleNewMenubarItems),
|
|
1526
1551
|
// default menu options listed at https://www.tiny.cloud/docs/configure/editor-appearance/#menu
|
|
1527
1552
|
// tinymce's default edit and table menus are fine
|
|
@@ -1536,10 +1561,10 @@ class RCEWrapper extends React.Component {
|
|
|
1536
1561
|
getToolbarForVariant(this.variant, this.ltiToolFavorites),
|
|
1537
1562
|
// @ts-expect-error
|
|
1538
1563
|
options.toolbar),
|
|
1539
|
-
contextmenu:
|
|
1564
|
+
contextmenu: '',
|
|
1540
1565
|
// show the browser's native context menu
|
|
1541
1566
|
|
|
1542
|
-
toolbar_mode:
|
|
1567
|
+
toolbar_mode: 'sliding',
|
|
1543
1568
|
toolbar_sticky: true,
|
|
1544
1569
|
// In regards to the ability to disable plugins:
|
|
1545
1570
|
// we only have to explicitly manage the removal of plugins
|
|
@@ -1548,16 +1573,16 @@ class RCEWrapper extends React.Component {
|
|
|
1548
1573
|
// handles all of that complexity. It that ever changes in the
|
|
1549
1574
|
// future in an upgraded version, we will have to update the
|
|
1550
1575
|
// logic in those other places as well.
|
|
1551
|
-
plugins: mergePlugins([
|
|
1576
|
+
plugins: mergePlugins(['autolink', 'media', 'table', 'link', 'directionality', 'lists', 'textpattern', 'hr', 'instructure_color', 'instructure-ui-icons', 'instructure_condensed_buttons', 'instructure_links', 'instructure_html_view', 'instructure_media_embed', 'a11y_checker', 'wordcount', 'instructure_wordcount', 'instructure_wordcount_header', 'instructure_keyboard_shortcuts_header', 'instructure_studio_media_options', 'instructure_rce_external_tools', ...pastePlugins, ...canvasPlugins],
|
|
1552
1577
|
// filter out the plugins designated for removal
|
|
1553
1578
|
// @ts-expect-error
|
|
1554
|
-
sanitizePlugins(options.plugins)?.filter(p => p.length > 0 && p[0] !==
|
|
1579
|
+
sanitizePlugins(options.plugins)?.filter(p => p.length > 0 && p[0] !== '-'), this.pluginsToExclude),
|
|
1555
1580
|
textpattern_patterns: [{
|
|
1556
|
-
start:
|
|
1557
|
-
cmd:
|
|
1581
|
+
start: '* ',
|
|
1582
|
+
cmd: 'InsertUnorderedList'
|
|
1558
1583
|
}, {
|
|
1559
|
-
start:
|
|
1560
|
-
cmd:
|
|
1584
|
+
start: '- ',
|
|
1585
|
+
cmd: 'InsertUnorderedList'
|
|
1561
1586
|
}]
|
|
1562
1587
|
};
|
|
1563
1588
|
if (this.props.trayProps) {
|
|
@@ -1579,7 +1604,7 @@ class RCEWrapper extends React.Component {
|
|
|
1579
1604
|
}
|
|
1580
1605
|
unhandleTextareaChange() {
|
|
1581
1606
|
if (this._textareaEl) {
|
|
1582
|
-
this._textareaEl.removeEventListener(
|
|
1607
|
+
this._textareaEl.removeEventListener('input', this.handleTextareaChange);
|
|
1583
1608
|
}
|
|
1584
1609
|
}
|
|
1585
1610
|
registerTextareaChange() {
|
|
@@ -1587,7 +1612,7 @@ class RCEWrapper extends React.Component {
|
|
|
1587
1612
|
if (this._textareaEl !== el) {
|
|
1588
1613
|
this.unhandleTextareaChange();
|
|
1589
1614
|
if (el) {
|
|
1590
|
-
el.addEventListener(
|
|
1615
|
+
el.addEventListener('input', this.handleTextareaChange);
|
|
1591
1616
|
if (this.props.textareaClassName) {
|
|
1592
1617
|
// split the string on whitespace because classList doesn't let you add multiple
|
|
1593
1618
|
// space seperated classes at a time but does let you add an array of them
|
|
@@ -1613,7 +1638,7 @@ class RCEWrapper extends React.Component {
|
|
|
1613
1638
|
// initialize the RCE when it gets close to entering the viewport
|
|
1614
1639
|
{
|
|
1615
1640
|
root: null,
|
|
1616
|
-
rootMargin:
|
|
1641
|
+
rootMargin: '200px 0px',
|
|
1617
1642
|
threshold: 0.0
|
|
1618
1643
|
});
|
|
1619
1644
|
// @ts-expect-error
|
|
@@ -1632,7 +1657,7 @@ class RCEWrapper extends React.Component {
|
|
|
1632
1657
|
this.focusCurrentView();
|
|
1633
1658
|
}
|
|
1634
1659
|
if (prevProps.readOnly !== this.props.readOnly) {
|
|
1635
|
-
this.mceInstance().mode.set(this.props.readOnly ?
|
|
1660
|
+
this.mceInstance().mode.set(this.props.readOnly ? 'readonly' : 'design');
|
|
1636
1661
|
}
|
|
1637
1662
|
}
|
|
1638
1663
|
}
|
|
@@ -1646,7 +1671,7 @@ class RCEWrapper extends React.Component {
|
|
|
1646
1671
|
this._tagTinymceAuxDiv();
|
|
1647
1672
|
this.registerTextareaChange();
|
|
1648
1673
|
// @ts-expect-error
|
|
1649
|
-
this._elementRef.current.addEventListener(
|
|
1674
|
+
this._elementRef.current.addEventListener('keydown', this.handleKey, true);
|
|
1650
1675
|
// give the textarea its initial size
|
|
1651
1676
|
this.onResize(null, {
|
|
1652
1677
|
deltaY: 0
|
|
@@ -1654,7 +1679,7 @@ class RCEWrapper extends React.Component {
|
|
|
1654
1679
|
// Preload the LTI Tools modal
|
|
1655
1680
|
// This helps with loading the favorited external tools
|
|
1656
1681
|
if (this.ltiToolFavorites.length > 0) {
|
|
1657
|
-
import(
|
|
1682
|
+
import('./plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog');
|
|
1658
1683
|
}
|
|
1659
1684
|
bridge.renderEditor(this);
|
|
1660
1685
|
}
|
|
@@ -1676,9 +1701,9 @@ class RCEWrapper extends React.Component {
|
|
|
1676
1701
|
fallback: /*#__PURE__*/React.createElement("div", {
|
|
1677
1702
|
style: {
|
|
1678
1703
|
height: this.state.height,
|
|
1679
|
-
display:
|
|
1680
|
-
justifyContent:
|
|
1681
|
-
alignItems:
|
|
1704
|
+
display: 'flex',
|
|
1705
|
+
justifyContent: 'center',
|
|
1706
|
+
alignItems: 'center'
|
|
1682
1707
|
}
|
|
1683
1708
|
}, /*#__PURE__*/React.createElement(Spinner, {
|
|
1684
1709
|
renderTitle: renderLoading,
|
|
@@ -1714,7 +1739,7 @@ class RCEWrapper extends React.Component {
|
|
|
1714
1739
|
ref: this._editorPlaceholderRef,
|
|
1715
1740
|
style: {
|
|
1716
1741
|
height: `${this.props.editorOptions.height}px`,
|
|
1717
|
-
border:
|
|
1742
|
+
border: '1px solid grey'
|
|
1718
1743
|
}
|
|
1719
1744
|
});
|
|
1720
1745
|
}
|
|
@@ -1740,8 +1765,8 @@ class RCEWrapper extends React.Component {
|
|
|
1740
1765
|
// @ts-expect-error
|
|
1741
1766
|
,
|
|
1742
1767
|
ref: this._elementRef,
|
|
1743
|
-
style: this.variant ===
|
|
1744
|
-
marginBottom:
|
|
1768
|
+
style: this.variant === 'full' ? {
|
|
1769
|
+
marginBottom: '.5rem'
|
|
1745
1770
|
} : undefined,
|
|
1746
1771
|
onFocus: this.handleFocusRCE,
|
|
1747
1772
|
onBlur: this.handleBlurRCE
|
|
@@ -1749,7 +1774,7 @@ class RCEWrapper extends React.Component {
|
|
|
1749
1774
|
id: `show-on-focus-btn-${this.id}`,
|
|
1750
1775
|
onClick: this.openKBShortcutModal,
|
|
1751
1776
|
margin: "xx-small",
|
|
1752
|
-
screenReaderLabel: formatMessage(
|
|
1777
|
+
screenReaderLabel: formatMessage('View keyboard shortcuts')
|
|
1753
1778
|
// @ts-expect-error
|
|
1754
1779
|
,
|
|
1755
1780
|
ref: el => this._showOnFocusButton = el
|
|
@@ -1761,7 +1786,7 @@ class RCEWrapper extends React.Component {
|
|
|
1761
1786
|
afterDismiss: this.removeAlert
|
|
1762
1787
|
}), this.state.editorView === PRETTY_HTML_EDITOR_VIEW && this.renderHtmlEditor(), /*#__PURE__*/React.createElement("div", {
|
|
1763
1788
|
style: {
|
|
1764
|
-
display: this.state.editorView === PRETTY_HTML_EDITOR_VIEW ?
|
|
1789
|
+
display: this.state.editorView === PRETTY_HTML_EDITOR_VIEW ? 'none' : 'block'
|
|
1765
1790
|
}
|
|
1766
1791
|
}, /*#__PURE__*/React.createElement(Editor, {
|
|
1767
1792
|
id: mceProps.textareaId,
|
|
@@ -1898,8 +1923,8 @@ RCEWrapper.defaultProps = {
|
|
|
1898
1923
|
maxInitRenderedRCEs: -1,
|
|
1899
1924
|
features: {},
|
|
1900
1925
|
timezone: Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,
|
|
1901
|
-
canvasOrigin:
|
|
1902
|
-
variant:
|
|
1926
|
+
canvasOrigin: '',
|
|
1927
|
+
variant: 'full'
|
|
1903
1928
|
};
|
|
1904
1929
|
RCEWrapper.skinCssInjected = false;
|
|
1905
1930
|
export default RCEWrapper;
|