@instructure/canvas-rce 7.2.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/es/enhance-user-content/doc_previews.js +6 -14
  3. package/es/enhance-user-content/enhance_user_content.js +1 -1
  4. package/es/enhance-user-content/instructure_helper.js +1 -0
  5. package/es/index.d.ts +1 -0
  6. package/es/index.js +2 -1
  7. package/es/rce/AlertMessageArea.js +1 -3
  8. package/es/rce/KeyboardShortcutModal.js +1 -1
  9. package/es/rce/RCEGlobals.d.ts +2 -0
  10. package/es/rce/RCEGlobals.js +1 -0
  11. package/es/rce/RCEVariants.js +3 -3
  12. package/es/rce/RCEWrapper.d.ts +3 -1
  13. package/es/rce/RCEWrapper.js +43 -11
  14. package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.d.ts +2 -0
  15. package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.js +45 -0
  16. package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.d.ts +1 -0
  17. package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.js +43 -0
  18. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.d.ts +1 -1
  19. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.js +2 -1
  20. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.d.ts +1 -1
  21. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.js +2 -1
  22. package/es/rce/plugins/instructure_record/mediaTranslations.js +1 -1
  23. package/es/rce/plugins/instructure_studio_media_options/plugin.js +111 -14
  24. package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.d.ts +5 -0
  25. package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.js +23 -0
  26. package/es/rce/plugins/instructure_wordcount/components/WordCountModal.js +1 -0
  27. package/es/rce/plugins/instructure_wordcount_header/plugin.d.ts +1 -0
  28. package/es/rce/plugins/instructure_wordcount_header/plugin.js +75 -0
  29. package/es/rce/plugins/shared/ContentSelection.d.ts +1 -2
  30. package/es/rce/plugins/shared/ContentSelection.js +1 -18
  31. package/es/rce/plugins/shared/StudioLtiSupportUtils.d.ts +10 -1
  32. package/es/rce/plugins/shared/StudioLtiSupportUtils.js +110 -1
  33. package/es/rce/plugins/shared/Upload/ComputerPanel.js +1 -1
  34. package/es/rce/plugins/shared/Upload/UploadFileModal.js +37 -4
  35. package/es/rce/plugins/shared/Upload/VideoUrlPanel.d.ts +15 -0
  36. package/es/rce/plugins/shared/Upload/VideoUrlPanel.js +51 -0
  37. package/es/rce/plugins/shared/Upload/videoValidationUtils.d.ts +7 -0
  38. package/es/rce/plugins/shared/Upload/videoValidationUtils.js +58 -0
  39. package/es/rce/plugins/shared/iframeUtils.d.ts +1 -0
  40. package/es/rce/plugins/shared/iframeUtils.js +37 -0
  41. package/es/rce/tinyRCE.js +2 -0
  42. package/es/sidebar/actions/upload.d.ts +1 -0
  43. package/es/sidebar/actions/upload.js +56 -0
  44. package/es/sidebar/containers/sidebarHandlers.d.ts +1 -0
  45. package/es/sidebar/containers/sidebarHandlers.js +2 -1
  46. package/es/translations/locales/ar.js +30 -6
  47. package/es/translations/locales/ca.js +30 -6
  48. package/es/translations/locales/cy.js +30 -6
  49. package/es/translations/locales/da-x-k12.js +30 -6
  50. package/es/translations/locales/da.js +30 -6
  51. package/es/translations/locales/de.js +30 -6
  52. package/es/translations/locales/el.js +6 -0
  53. package/es/translations/locales/en-AU-x-unimelb.js +30 -6
  54. package/es/translations/locales/en-GB-x-ukhe.js +30 -6
  55. package/es/translations/locales/en.js +36 -6
  56. package/es/translations/locales/en_AU.js +30 -6
  57. package/es/translations/locales/en_CA.js +30 -6
  58. package/es/translations/locales/en_CY.js +30 -6
  59. package/es/translations/locales/en_GB.js +30 -6
  60. package/es/translations/locales/es.js +30 -6
  61. package/es/translations/locales/es_ES.js +30 -6
  62. package/es/translations/locales/fa_IR.js +6 -3
  63. package/es/translations/locales/fi.js +30 -6
  64. package/es/translations/locales/fr.js +30 -6
  65. package/es/translations/locales/fr_CA.js +34 -10
  66. package/es/translations/locales/ga.js +30 -6
  67. package/es/translations/locales/he.js +6 -0
  68. package/es/translations/locales/hi.js +30 -6
  69. package/es/translations/locales/ht.js +30 -6
  70. package/es/translations/locales/hu.js +6 -6
  71. package/es/translations/locales/hy.js +6 -0
  72. package/es/translations/locales/id.js +30 -6
  73. package/es/translations/locales/is.js +36 -6
  74. package/es/translations/locales/it.js +30 -6
  75. package/es/translations/locales/ja.js +30 -6
  76. package/es/translations/locales/ko.js +6 -0
  77. package/es/translations/locales/mi.js +30 -6
  78. package/es/translations/locales/ms.js +30 -6
  79. package/es/translations/locales/nb-x-k12.js +30 -6
  80. package/es/translations/locales/nb.js +30 -6
  81. package/es/translations/locales/nl.js +30 -6
  82. package/es/translations/locales/nn.js +6 -6
  83. package/es/translations/locales/pl.js +30 -6
  84. package/es/translations/locales/pt.js +30 -6
  85. package/es/translations/locales/pt_BR.js +30 -6
  86. package/es/translations/locales/ru.js +30 -6
  87. package/es/translations/locales/sl.js +30 -6
  88. package/es/translations/locales/sv-x-k12.js +30 -6
  89. package/es/translations/locales/sv.js +30 -6
  90. package/es/translations/locales/th.js +30 -6
  91. package/es/translations/locales/tr.js +6 -3
  92. package/es/translations/locales/uk_UA.js +6 -3
  93. package/es/translations/locales/vi.js +30 -6
  94. package/es/translations/locales/zh-Hans.js +30 -6
  95. package/es/translations/locales/zh-Hant.js +30 -6
  96. package/es/translations/locales/zh.js +30 -6
  97. package/es/translations/locales/zh_HK.js +30 -6
  98. package/package.json +53 -53
package/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 7.3.1 - 2025-11-11
9
+
10
+ ### Fixed
11
+ - WordCountModal not closing with keyboard navigation
12
+ - Accessibility issue for RCE's file preview
13
+ - Assessment questions preview path
14
+ - Formatting in Biome configuration for RCE package
15
+
16
+ ### Changed
17
+ - Updated TinyMCE shortcut link text
18
+
19
+ ### Localization
20
+ - Updated RCE translations
21
+
22
+ ## 7.3.0 - 2025-10-16
23
+
24
+ ### Added
25
+ - Block Content Editor (BCE) support and improvements
26
+ - Media Embed by URL functionality
27
+ - A11y checker for block editor
28
+ - User choices to improved Studio toolbar options
29
+ - Feature flag for upcoming Studio embed improvements
30
+
31
+ ### Changed
32
+ - Upgrade to InstUI 10.26.2
33
+ - Unify image upload buttons
34
+ - Adjust the text block footer
35
+ - Remove documents from RCE's toolbar for BCE
36
+
37
+ ### Fixed
38
+ - Prevent setState from TinyMCE events after unmount
39
+ - Image upload modal for BCE
40
+ - Revert Alert conditional rendering
41
+ - Revert axios version upgrade
42
+
43
+ ### Removed
44
+ - Remove crocodoc from canvas
45
+
46
+ ### Localization
47
+ - Updated RCE translations for multiple locales
48
+
8
49
  ## 7.2.0 - 2025-08-21
9
50
 
10
51
  ### Added
@@ -106,20 +106,10 @@ export function loadDocPreview($container, options) {
106
106
  });
107
107
  }
108
108
  }
109
- if (opts.crocodoc_session_url) {
110
- const sanitizedUrl = sanitizeUrl(opts.crocodoc_session_url);
111
- const iframe = document.createElement('iframe');
112
- iframe.setAttribute('src', sanitizedUrl);
113
- iframe.setAttribute('width', opts.width);
114
- iframe.setAttribute('height', opts.height);
115
- iframe.setAttribute('allowfullscreen', '1');
116
- iframe.id = opts.id;
117
- $container.appendChild(iframe);
118
- iframe.load(() => {
119
- tellAppIViewedThisInline('crocodoc');
120
- if (typeof opts.ready === 'function') opts.ready();
121
- });
122
- } else if (opts.canvadoc_session_url) {
109
+ const iframeAriaLabel = opts.attachment_name ? formatMessage('File preview for {fileName}', {
110
+ fileName: opts.attachment_name
111
+ }) : formatMessage('File preview');
112
+ if (opts.canvadoc_session_url) {
123
113
  const canvadocWrapper = document.createElement('div');
124
114
  canvadocWrapper.setAttribute('style', 'overflow: auto; resize: vertical; border: 1px solid transparent; height: 100%;');
125
115
  $container.appendChild(canvadocWrapper);
@@ -130,6 +120,7 @@ export function loadDocPreview($container, options) {
130
120
  tellAppIViewedThisInline('canvadocs');
131
121
  if (typeof opts.ready === 'function') opts.ready();
132
122
  });
123
+ iframe.setAttribute('aria-label', iframeAriaLabel);
133
124
  iframe.setAttribute('src', sanitizedUrl);
134
125
  iframe.setAttribute('width', opts.width);
135
126
  iframe.setAttribute('allowfullscreen', '1');
@@ -155,6 +146,7 @@ export function loadDocPreview($container, options) {
155
146
  iframe.setAttribute('src', googleDocPreviewUrl);
156
147
  iframe.setAttribute('height', opts.height);
157
148
  iframe.setAttribute('width', '100%');
149
+ iframe.setAttribute('aria-label', iframeAriaLabel);
158
150
  $container.appendChild(iframe);
159
151
  }
160
152
  };
@@ -269,7 +269,7 @@ export function enhanceUserContent(container = document, opts = {}) {
269
269
 
270
270
  // Don't attempt to enhance links with no href
271
271
  if (!href) return;
272
- const matchesCanvasFile = href.pathname.match(/(?:\/(courses|groups|users)\/\d+)?\/files\/([\d~]+)(?=[!*'();:@&=+$,/?#\[\]]|$)/);
272
+ const matchesCanvasFile = href.pathname.match(/(?:\/(courses|groups|users|assessment_questions)\/\d+)?\/files\/([\d~]+)(?=[!*'();:@&=+$,/?#\[\]]|$)/);
273
273
  if (!matchesCanvasFile) {
274
274
  // a bug in the new RCE added instructure_file_link class name to all links
275
275
  // only proceed if this is a canvas file link
@@ -157,6 +157,7 @@ export function showFilePreviewInline(event, canvasOrigin, disableGooglePreviews
157
157
  mimeType: attachment.content_type,
158
158
  public_url: attachment.public_url,
159
159
  attachment_preview_processing: attachment.workflow_state === 'pending_upload' || attachment.workflow_state === 'processing',
160
+ attachment_name: attachment.display_name,
160
161
  disableGooglePreviews
161
162
  });
162
163
  const $minimizeLink = document.createElement('a');
package/es/index.d.ts CHANGED
@@ -57,3 +57,4 @@ export declare function getRCSAuthenticationHeaders(jwt: string): {
57
57
  Authorization: string;
58
58
  };
59
59
  export declare function getRCSOriginFromHost(host: string): any;
60
+ export { default as checkNode } from './rce/plugins/tinymce-a11y-checker/node-checker';
package/es/index.js CHANGED
@@ -50,4 +50,5 @@ export function getRCSAuthenticationHeaders(jwt) {
50
50
  }
51
51
  export function getRCSOriginFromHost(host) {
52
52
  return originFromHost(host);
53
- }
53
+ }
54
+ export { default as checkNode } from './rce/plugins/tinymce-a11y-checker/node-checker';
@@ -29,9 +29,7 @@ export default function AlertMessageArea({
29
29
  return /*#__PURE__*/React.createElement("div", null, messages.map(message => /*#__PURE__*/React.createElement(Alert, {
30
30
  key: message.id,
31
31
  variant: message.variant || 'info',
32
- timeout: 10000
33
- // @ts-expect-error
34
- ,
32
+ timeout: 10000,
35
33
  liveRegion: liveRegion,
36
34
  onDismiss: () => afterDismiss(message.id)
37
35
  }, message.text)));
@@ -79,7 +79,7 @@ export default function KeyboardShortcutModal(props) {
79
79
  }, formatMessage('Other editor shortcuts may be found at'), ' ', /*#__PURE__*/React.createElement("a", {
80
80
  href: "https://www.tiny.cloud/docs/advanced/keyboard-shortcuts/",
81
81
  target: "rcekbshortcut"
82
- }, "https://www.tiny.cloud/docs/advanced/keyboard-shortcuts/")))));
82
+ }, "TinyMCE Keyboard Shortcuts")))));
83
83
  }
84
84
  KeyboardShortcutModal.propTypes = {
85
85
  open: bool.isRequired,
@@ -1,11 +1,13 @@
1
1
  export default instance;
2
2
  export type Features = {
3
3
  file_verifiers_for_quiz_links: boolean;
4
+ rce_studio_embed_improvements: boolean;
4
5
  };
5
6
  declare const instance: RCEGlobals;
6
7
  /**
7
8
  * @typedef {Object} Features
8
9
  * @property {boolean} file_verifiers_for_quiz_links
10
+ * @property {boolean} rce_studio_embed_improvements
9
11
  */
10
12
  declare class RCEGlobals {
11
13
  _data: {
@@ -21,6 +21,7 @@ const isEmpty = obj => Object.keys(obj).length === 0;
21
21
  /**
22
22
  * @typedef {Object} Features
23
23
  * @property {boolean} file_verifiers_for_quiz_links
24
+ * @property {boolean} rce_studio_embed_improvements
24
25
  */
25
26
 
26
27
  class RCEGlobals {
@@ -110,13 +110,13 @@ export function getToolbarForVariant(variant, ltiToolFavorites = []) {
110
110
  items: ['bold', 'italic', 'underline', 'instructure_color', 'inst_subscript', 'inst_superscript']
111
111
  }, {
112
112
  name: formatMessage('Content'),
113
- items: ['instructure_links', 'instructure_documents']
113
+ items: ['instructure_links']
114
114
  }, {
115
115
  name: formatMessage('Alignment and Lists'),
116
116
  items: ['align', 'bullist', 'inst_indent', 'inst_outdent']
117
117
  }, {
118
118
  name: formatMessage('Miscellaneous'),
119
- items: ['removeformat', 'instructure_equation']
119
+ items: ['removeformat', 'instructure_equation', 'instructure_keyboard_shortcuts_header', 'instructure_wordcount_header']
120
120
  }];
121
121
  }
122
122
  return [{
@@ -152,7 +152,7 @@ export function getStatusBarFeaturesForVariant(variant, options = {
152
152
  return [];
153
153
  }
154
154
  if (variant === 'block-content-editor') {
155
- return ['keyboard_shortcuts', 'word_count'];
155
+ return [];
156
156
  }
157
157
  const platformFeatures = options.isDesktop ? DESKTOP_FEATURES : MOBILE_FEATURES;
158
158
  if (variant === 'lite' || variant === 'text-only') {
@@ -7,7 +7,7 @@ import EncryptedStorage from '../util/encrypted-storage';
7
7
  import { RCEVariant } from './RCEVariants';
8
8
  import { mergeMenu, mergeMenuItems, mergePlugins, mergeToolbar, parsePluginsToExclude } from './RCEWrapper.utils';
9
9
  import { AlertMessage, EditorOptions, RCETrayProps } from './types';
10
- export declare function storageAvailable(): boolean | undefined;
10
+ export declare function storageAvailable(): boolean | null;
11
11
  interface RCEWrapperProps {
12
12
  ai_text_tools?: boolean;
13
13
  autosave?: {
@@ -214,6 +214,7 @@ declare class RCEWrapper extends React.Component<RCEWrapperProps, RCEWrapperStat
214
214
  new_math_equation_handling: unknown;
215
215
  explicit_latex_typesetting: unknown;
216
216
  rce_transform_loaded_content: unknown;
217
+ rce_studio_embed_improvements: unknown;
217
218
  file_verifiers_for_quiz_links: unknown;
218
219
  rce_find_replace: unknown;
219
220
  consolidated_media_player: unknown;
@@ -315,6 +316,7 @@ declare class RCEWrapper extends React.Component<RCEWrapperProps, RCEWrapperStat
315
316
  */
316
317
  _setupSelectionSaving: (editor: any) => void;
317
318
  announcing: number;
319
+ _isMounted: boolean;
318
320
  announceContextToolbars(editor: TinyMCEEditor): void;
319
321
  initAutoSave: (editor: TinyMCEEditor) => void;
320
322
  cleanupAutoSave: (deleteAll?: boolean) => void;
@@ -102,7 +102,7 @@ const editorWrappers = new WeakMap();
102
102
  // determines if localStorage is available for our use.
103
103
  // see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
104
104
  export function storageAvailable() {
105
- let storage;
105
+ let storage = null;
106
106
  try {
107
107
  storage = window.localStorage;
108
108
  const x = '__storage_test__';
@@ -219,7 +219,9 @@ class RCEWrapper extends React.Component {
219
219
  window.visualViewport?.addEventListener('resize', this._handleFullscreenResize);
220
220
  this._handleFullscreenResize();
221
221
  // @ts-expect-error
222
- this._focusRegion = FocusRegionManager.activateRegion(document[FS_ELEMENT], {
222
+ this._focusRegion = FocusRegionManager.activateRegion(
223
+ // @ts-expect-error
224
+ document[FS_ELEMENT], {
223
225
  shouldContainFocus: true
224
226
  });
225
227
  } else {
@@ -332,6 +334,31 @@ class RCEWrapper extends React.Component {
332
334
  tinyapp.setAttribute('tabIndex', '-1');
333
335
  }
334
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);
360
+ }
361
+
335
362
  // Probably should do this in tinymce.scss, but we only want it in new rce
336
363
  textarea.style.resize = 'none';
337
364
  editor.on('keydown', this.handleKey);
@@ -463,6 +490,7 @@ class RCEWrapper extends React.Component {
463
490
  });
464
491
  };
465
492
  this.announcing = 0;
493
+ this._isMounted = false;
466
494
  /* ********** autosave support *************** */
467
495
  this.initAutoSave = editor => {
468
496
  var _this$props$userCache;
@@ -867,6 +895,7 @@ class RCEWrapper extends React.Component {
867
895
  explicit_latex_typesetting = false,
868
896
  rce_transform_loaded_content = false,
869
897
  rce_find_replace = false,
898
+ rce_studio_embed_improvements = false,
870
899
  file_verifiers_for_quiz_links = false,
871
900
  consolidated_media_player = false
872
901
  } = this.props.features;
@@ -874,6 +903,7 @@ class RCEWrapper extends React.Component {
874
903
  new_math_equation_handling,
875
904
  explicit_latex_typesetting,
876
905
  rce_transform_loaded_content,
906
+ rce_studio_embed_improvements,
877
907
  file_verifiers_for_quiz_links,
878
908
  rce_find_replace,
879
909
  consolidated_media_player
@@ -1323,10 +1353,12 @@ class RCEWrapper extends React.Component {
1323
1353
  // focus is still somewhere w/in me
1324
1354
  return;
1325
1355
  }
1326
- const activeClass = document.activeElement && document.activeElement.getAttribute('class');
1356
+ const activeClass = document.activeElement?.getAttribute('class');
1327
1357
  if (
1328
1358
  // @ts-expect-error
1329
- (event.focusedEditor === undefined || event.target.id === event.focusedEditor?.id) && activeClass?.includes('tox-')) {
1359
+ (event.focusedEditor === undefined ||
1360
+ // @ts-expect-error
1361
+ event.target.id === event.focusedEditor?.id) && activeClass?.includes('tox-')) {
1330
1362
  // if a toolbar button has focus, then the user clicks on the "more" button
1331
1363
  // focus jumps to the body, then eventually to the popped up toolbar. This
1332
1364
  // catches that case.
@@ -1362,6 +1394,7 @@ class RCEWrapper extends React.Component {
1362
1394
  }
1363
1395
  announceContextToolbars(editor) {
1364
1396
  editor.on('NodeChange', () => {
1397
+ if (!this._isMounted) return;
1365
1398
  const node = editor.selection.getNode();
1366
1399
  // @ts-expect-error
1367
1400
  if (isImageEmbed(node, editor)) {
@@ -1401,7 +1434,7 @@ class RCEWrapper extends React.Component {
1401
1434
  editor.on('ResizeEditor', ({
1402
1435
  deltaY
1403
1436
  }) => {
1404
- if (!deltaY) return;
1437
+ if (!this._isMounted || !deltaY) return;
1405
1438
  if (deltaY < 0) {
1406
1439
  this.setState({
1407
1440
  announcement: formatMessage('The height of Rich Content Area is decreased.')
@@ -1441,6 +1474,7 @@ class RCEWrapper extends React.Component {
1441
1474
  return `rceautosave:${userId}${window.location.href}:${this.props.textareaId}`;
1442
1475
  }
1443
1476
  componentWillUnmount() {
1477
+ this._isMounted = false;
1444
1478
  if (this.state.shouldShowEditor) {
1445
1479
  window.clearTimeout(this.blurTimer);
1446
1480
  if (!this._destroyCalled) {
@@ -1539,7 +1573,7 @@ class RCEWrapper extends React.Component {
1539
1573
  // handles all of that complexity. It that ever changes in the
1540
1574
  // future in an upgraded version, we will have to update the
1541
1575
  // logic in those other places as well.
1542
- 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_studio_media_options', 'instructure_rce_external_tools', ...pastePlugins, ...canvasPlugins],
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],
1543
1577
  // filter out the plugins designated for removal
1544
1578
  // @ts-expect-error
1545
1579
  sanitizePlugins(options.plugins)?.filter(p => p.length > 0 && p[0] !== '-'), this.pluginsToExclude),
@@ -1589,6 +1623,7 @@ class RCEWrapper extends React.Component {
1589
1623
  }
1590
1624
  }
1591
1625
  componentDidMount() {
1626
+ this._isMounted = true;
1592
1627
  if (this.state.shouldShowEditor) {
1593
1628
  this.editorReallyDidMount();
1594
1629
  } else {
@@ -1841,13 +1876,10 @@ class RCEWrapper extends React.Component {
1841
1876
  open: this.state.confirmAutoSave,
1842
1877
  onNo: () => this.restoreAutoSave(false),
1843
1878
  onYes: () => this.restoreAutoSave(true)
1844
- })) : null, this.state.announcement &&
1845
- /*#__PURE__*/
1846
- // @ts-expect-error
1847
- React.createElement(Alert, {
1879
+ })) : null, /*#__PURE__*/React.createElement(Alert, {
1848
1880
  screenReaderOnly: true,
1849
1881
  liveRegion: this.props.liveRegion
1850
- }, this.state.announcement));
1882
+ }, this.state.announcement || null));
1851
1883
  }));
1852
1884
  }
1853
1885
  }
@@ -0,0 +1,2 @@
1
+ import { Editor } from 'tinymce';
2
+ export default function (ed: Editor, document: Document): Promise<void>;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (C) 2024 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import React from 'react';
20
+ import ReactDOM from 'react-dom';
21
+ const MODAL_ID = 'canvas-rce-keyboard-shortcuts-container';
22
+ export default function (ed, document) {
23
+ return import('../../KeyboardShortcutModal').then(({
24
+ default: KeyboardShortcutModal
25
+ }) => {
26
+ let container = document.querySelector(`#${MODAL_ID}`);
27
+ if (!container) {
28
+ container = document.createElement('div');
29
+ container.id = MODAL_ID;
30
+ document.body.appendChild(container);
31
+ }
32
+ const handleDismiss = () => {
33
+ if (container) {
34
+ ReactDOM.unmountComponentAtNode(container);
35
+ }
36
+ ed.focus();
37
+ };
38
+ ReactDOM.render(/*#__PURE__*/React.createElement(KeyboardShortcutModal, {
39
+ open: true,
40
+ onClose: handleDismiss,
41
+ onDismiss: handleDismiss,
42
+ onExited: handleDismiss
43
+ }), container);
44
+ });
45
+ }
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright (C) 2024 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import formatMessage from '../../../format-message';
20
+ // @ts-expect-error
21
+ import { IconKeyboardShortcutsLine } from '@instructure/ui-icons/es/svg';
22
+
23
+ // Dynamically import the callback to avoid module resolution issues
24
+ const clickCallbackPromise = import('./clickCallback');
25
+
26
+ // @ts-expect-error: tinymce is available as a global variable
27
+ tinymce.PluginManager.add('instructure_keyboard_shortcuts_header', function (ed) {
28
+ // Register custom icon
29
+ ed.ui.registry.addIcon('keyboard-shortcuts', IconKeyboardShortcutsLine.src);
30
+ ed.addCommand('instructureKeyboardShortcuts', () => {
31
+ clickCallbackPromise.then(module => module.default(ed, document));
32
+ });
33
+ ed.ui.registry.addButton('instructure_keyboard_shortcuts_header', {
34
+ icon: 'keyboard-shortcuts',
35
+ tooltip: formatMessage('View keyboard shortcuts'),
36
+ onAction: () => ed.execCommand('instructureKeyboardShortcuts')
37
+ });
38
+ ed.ui.registry.addMenuItem('instructure_keyboard_shortcuts_header', {
39
+ icon: 'keyboard-shortcuts',
40
+ text: formatMessage('Keyboard Shortcuts'),
41
+ onAction: () => ed.execCommand('instructureKeyboardShortcuts')
42
+ });
43
+ });
@@ -3,7 +3,7 @@ export default class TrayController {
3
3
  _isOpen: boolean;
4
4
  _shouldOpen: boolean;
5
5
  _editor: any;
6
- _audioContainer: any;
6
+ _audioContainer: Element | null;
7
7
  get container(): HTMLElement;
8
8
  get isOpen(): boolean;
9
9
  showTrayForEditor(editor: any): void;
@@ -19,7 +19,8 @@
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
- import { asAudioElement, findMediaPlayerIframe } from '../../shared/ContentSelection';
22
+ import { asAudioElement } from '../../shared/ContentSelection';
23
+ import { findMediaPlayerIframe } from '../../shared/iframeUtils';
23
24
  import AudioOptionsTray from '.';
24
25
  export const CONTAINER_ID = 'instructure-audio-options-tray-container';
25
26
  export default class TrayController {
@@ -27,7 +27,7 @@ export default class TrayController {
27
27
  get $container(): HTMLElement;
28
28
  get isOpen(): boolean;
29
29
  showTrayForEditor(editor: any): void;
30
- $videoContainer: any;
30
+ $videoContainer: Element | null | undefined;
31
31
  hideTrayForEditor(editor: any): void;
32
32
  _applyVideoOptions(videoOptions: any): void;
33
33
  _dismissTray(): void;
@@ -19,7 +19,8 @@
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
- import { asVideoElement, findMediaPlayerIframe } from '../../shared/ContentSelection';
22
+ import { asVideoElement } from '../../shared/ContentSelection';
23
+ import { findMediaPlayerIframe } from '../../shared/iframeUtils';
23
24
  import VideoOptionsTray from '.';
24
25
  import { isStudioEmbeddedMedia, parseStudioOptions } from '../../shared/StudioLtiSupportUtils';
25
26
  import RCEGlobals from '../../../RCEGlobals';
@@ -24,7 +24,7 @@ import formatMessage from '../../../format-message';
24
24
  const uploadMediaTranslations = {
25
25
  UploadMediaStrings: {
26
26
  ADD_CLOSED_CAPTIONS_OR_SUBTITLES: formatMessage('Add CC/Subtitles'),
27
- CLEAR_FILE_TEXT: formatMessage('Clear selected file'),
27
+ CLEAR_FILE_TEXT: formatMessage('Remove'),
28
28
  CLOSE_TEXT: formatMessage('Close'),
29
29
  CLOSED_CAPTIONS_CHOOSE_FILE: formatMessage('Choose caption file'),
30
30
  CLOSED_CAPTIONS_SELECT_LANGUAGE: formatMessage('Select Language'),
@@ -17,24 +17,121 @@
17
17
  */
18
18
 
19
19
  import tinymce from 'tinymce';
20
- import { isStudioEmbeddedMedia, handleBeforeObjectSelected } from '../shared/StudioLtiSupportUtils';
20
+ import { isStudioEmbeddedMedia, handleBeforeObjectSelected, notifyStudioEmbedTypeChange, updateStudioIframeDimensions, isValidDimension, isValidEmbedType, isValidResizable } from '../shared/StudioLtiSupportUtils';
21
21
  import VideoTrayController from '../instructure_record/VideoOptionsTray/TrayController';
22
22
  import formatMessage from '../../../format-message';
23
+ import RCEGlobals from '../../RCEGlobals';
24
+ import { thumbnailViewIcon, learnViewIcon, collabViewIcon, optionsIcon, removeIcon } from './studioToolbarIcons';
23
25
  const studioTrayController = new VideoTrayController();
26
+ const handleStudioMessage = e => {
27
+ if (e.data && e.data.subject === 'studio.embedTypeChanged.response' && isValidDimension(e.data.width) && isValidDimension(e.data.height) && isValidEmbedType(e.data.embedType)) {
28
+ // resizable is optional - only pass it if it's a valid boolean
29
+ const resizable = isValidResizable(e.data.resizable) ? e.data.resizable : undefined;
30
+ updateStudioIframeDimensions(tinymce.activeEditor, e.data.width, e.data.height, e.data.embedType, resizable);
31
+ }
32
+ };
24
33
  tinymce.PluginManager.add('instructure_studio_media_options', function (ed) {
25
- ed.ui.registry.addButton('studio-media-options', {
26
- onAction() {
27
- studioTrayController.showTrayForEditor(ed);
28
- },
29
- text: formatMessage('Studio Media Options'),
30
- tooltip: formatMessage('Show Studio media options')
31
- });
32
- ed.ui.registry.addContextToolbar('studio-media-options-toolbar', {
33
- items: 'studio-media-options',
34
- position: 'node',
35
- predicate: isStudioEmbeddedMedia,
36
- scope: 'node'
37
- });
34
+ if (RCEGlobals.getFeatures().rce_studio_embed_improvements) {
35
+ window.addEventListener('message', handleStudioMessage);
36
+ ed.on('init', () => {
37
+ const existingStyle = document.getElementById('studio-toolbar-styles');
38
+ if (!existingStyle) {
39
+ const style = document.createElement('style');
40
+ style.id = 'studio-toolbar-styles';
41
+ style.textContent = `
42
+ .tox .tox-pop .tox-tbtn {
43
+ font-size: 16px;
44
+ border-radius: 0;
45
+ }
46
+
47
+ .tox .tox-pop .tox-tbtn:hover {
48
+ background-color: #2B7ABC;
49
+ color: white;
50
+ }
51
+ `;
52
+ document.head.appendChild(style);
53
+ }
54
+ });
55
+ ed.ui.registry.addIcon('thumbnail-view-icon', thumbnailViewIcon);
56
+ ed.ui.registry.addIcon('learn-view-icon', learnViewIcon);
57
+ ed.ui.registry.addIcon('collab-view-icon', collabViewIcon);
58
+ ed.ui.registry.addIcon('options-icon', optionsIcon);
59
+ ed.ui.registry.addIcon('remove-icon', removeIcon);
60
+ ed.ui.registry.addButton('thumbnail-view', {
61
+ onAction() {
62
+ notifyStudioEmbedTypeChange(ed, 'thumbnail_embed');
63
+ },
64
+ icon: 'thumbnail-view-icon',
65
+ text: formatMessage('Thumbnail'),
66
+ tooltip: formatMessage('Thumbnail')
67
+ });
68
+ ed.ui.registry.addButton('learn-view', {
69
+ onAction() {
70
+ notifyStudioEmbedTypeChange(ed, 'learn_embed');
71
+ },
72
+ icon: 'learn-view-icon',
73
+ text: formatMessage('Learn'),
74
+ tooltip: formatMessage('Learn')
75
+ });
76
+ ed.ui.registry.addButton('collab-view', {
77
+ onAction() {
78
+ notifyStudioEmbedTypeChange(ed, 'collaboration_embed');
79
+ },
80
+ icon: 'collab-view-icon',
81
+ text: formatMessage('Collab'),
82
+ tooltip: formatMessage('Collab')
83
+ });
84
+ ed.ui.registry.addButton('studio-media-options', {
85
+ onAction() {
86
+ if (!studioTrayController.isOpen) {
87
+ studioTrayController.showTrayForEditor(ed);
88
+ }
89
+ },
90
+ icon: 'options-icon',
91
+ text: formatMessage('Options'),
92
+ tooltip: formatMessage('Options')
93
+ });
94
+ ed.ui.registry.addButton('remove-studio-media', {
95
+ onAction() {
96
+ const selectedElement = ed.selection.getNode();
97
+ if (selectedElement && isStudioEmbeddedMedia(selectedElement)) {
98
+ studioTrayController.hideTrayForEditor(ed);
99
+
100
+ // Hide toolbar, reset selection
101
+ ed.fire('hidecontexttoolbar');
102
+ ed.dom.remove(selectedElement);
103
+ ed.nodeChanged();
104
+ ed.selection.select(ed.getBody());
105
+
106
+ // Force focus back to editor
107
+ ed.focus();
108
+ }
109
+ },
110
+ icon: 'remove-icon',
111
+ text: formatMessage('Remove'),
112
+ tooltip: formatMessage('Remove Studio Media')
113
+ });
114
+ ed.ui.registry.addContextToolbar('studio-extra-toolbar', {
115
+ items: 'thumbnail-view | learn-view | collab-view | studio-media-options | remove-studio-media',
116
+ position: 'node',
117
+ predicate: isStudioEmbeddedMedia,
118
+ scope: 'node'
119
+ });
120
+ } else {
121
+ ed.ui.registry.addButton('studio-media-options', {
122
+ onAction() {
123
+ studioTrayController.showTrayForEditor(ed);
124
+ },
125
+ text: formatMessage('Studio Media Options'),
126
+ tooltip: formatMessage('Show Studio media options')
127
+ });
128
+ ed.ui.registry.addContextToolbar('studio-media-options-toolbar', {
129
+ items: 'studio-media-options',
130
+ position: 'node',
131
+ predicate: isStudioEmbeddedMedia,
132
+ scope: 'node'
133
+ });
134
+ }
38
135
  ed.on('BeforeObjectSelected', handleBeforeObjectSelected);
39
136
  ed.on('remove', editor => {
40
137
  studioTrayController.hideTrayForEditor(editor);