@instructure/canvas-rce 5.12.2 → 5.13.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 (84) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/es/common/fileUrl.js +5 -1
  3. package/es/defaultTinymceConfig.js +2 -1
  4. package/es/enhance-user-content/enhance_user_content.js +30 -1
  5. package/es/getTranslations.js +5 -1
  6. package/es/rce/RCEWrapper.js +63 -22
  7. package/es/rce/RCEWrapperProps.js +1 -0
  8. package/es/rce/StatusBar.js +5 -4
  9. package/es/rce/editorLanguage.js +2 -0
  10. package/es/rce/plugins/instructure_image/ImageEmbedOptions.js +1 -1
  11. package/es/rce/plugins/instructure_rce_external_tools/ExternalToolsEnv.js +6 -0
  12. package/es/rce/plugins/instructure_rce_external_tools/RceToolWrapper.js +30 -1
  13. package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolDialog/ExternalToolDialog.js +14 -1
  14. package/es/rce/plugins/instructure_rce_external_tools/lti13-content-items/processEditorContentItems.js +9 -3
  15. package/es/rce/plugins/instructure_rce_external_tools/plugin.js +3 -2
  16. package/es/rce/plugins/instructure_search_and_replace/clickCallback.js +55 -0
  17. package/es/rce/plugins/instructure_search_and_replace/components/FindReplaceTray.js +360 -0
  18. package/es/rce/plugins/instructure_search_and_replace/components/FindReplaceTrayController.js +139 -0
  19. package/es/rce/plugins/instructure_search_and_replace/getSelectionContext.js +68 -0
  20. package/es/rce/plugins/instructure_search_and_replace/plugin.js +39 -0
  21. package/es/rce/plugins/instructure_search_and_replace/types.d.js +1 -0
  22. package/es/rce/plugins/shared/fileTypeUtils.js +3 -1
  23. package/es/rce/plugins/tinymce-a11y-checker/plugin.js +11 -1
  24. package/es/rce/plugins/tinymce-a11y-checker/utils/indicate.js +1 -1
  25. package/es/rce/tinyRCE.js +3 -1
  26. package/es/translations/locales/ar.js +54 -0
  27. package/es/translations/locales/ca.js +54 -0
  28. package/es/translations/locales/cy.js +54 -0
  29. package/es/translations/locales/da-x-k12.js +54 -0
  30. package/es/translations/locales/da.js +54 -0
  31. package/es/translations/locales/de.js +54 -0
  32. package/es/translations/locales/el.js +9 -0
  33. package/es/translations/locales/en-AU-x-unimelb.js +54 -0
  34. package/es/translations/locales/en-GB-x-ukhe.js +54 -0
  35. package/es/translations/locales/en.js +54 -0
  36. package/es/translations/locales/en_AU.js +54 -0
  37. package/es/translations/locales/en_CA.js +54 -0
  38. package/es/translations/locales/en_CY.js +54 -0
  39. package/es/translations/locales/en_GB.js +54 -0
  40. package/es/translations/locales/es.js +54 -0
  41. package/es/translations/locales/es_ES.js +54 -0
  42. package/es/translations/locales/fa_IR.js +9 -0
  43. package/es/translations/locales/fi.js +54 -0
  44. package/es/translations/locales/fr.js +54 -0
  45. package/es/translations/locales/fr_CA.js +54 -0
  46. package/es/translations/locales/ga.js +2427 -0
  47. package/es/translations/locales/he.js +10 -1
  48. package/es/translations/locales/ht.js +54 -0
  49. package/es/translations/locales/hu.js +15 -0
  50. package/es/translations/locales/hy.js +9 -0
  51. package/es/translations/locales/id.js +55 -0
  52. package/es/translations/locales/id_ID.js +1 -0
  53. package/es/translations/locales/is.js +54 -0
  54. package/es/translations/locales/it.js +54 -0
  55. package/es/translations/locales/ja.js +61 -7
  56. package/es/translations/locales/ko.js +9 -0
  57. package/es/translations/locales/mi.js +54 -0
  58. package/es/translations/locales/ms.js +54 -0
  59. package/es/translations/locales/nb-x-k12.js +54 -0
  60. package/es/translations/locales/nb.js +54 -0
  61. package/es/translations/locales/nl.js +54 -0
  62. package/es/translations/locales/nn.js +9 -0
  63. package/es/translations/locales/pl.js +54 -0
  64. package/es/translations/locales/pt.js +54 -0
  65. package/es/translations/locales/pt_BR.js +54 -0
  66. package/es/translations/locales/ru.js +54 -0
  67. package/es/translations/locales/sl.js +54 -0
  68. package/es/translations/locales/sv-x-k12.js +54 -0
  69. package/es/translations/locales/sv.js +54 -0
  70. package/es/translations/locales/th.js +54 -0
  71. package/es/translations/locales/tr.js +9 -0
  72. package/es/translations/locales/uk_UA.js +9 -0
  73. package/es/translations/locales/vi.js +54 -0
  74. package/es/translations/locales/zh-Hans.js +54 -0
  75. package/es/translations/locales/zh-Hant.js +54 -0
  76. package/es/translations/locales/zh.js +54 -0
  77. package/es/translations/locales/zh_HK.js +54 -0
  78. package/es/translations/tinymce/ga.js +423 -0
  79. package/es/translations/tinymce/id.js +423 -0
  80. package/es/translations/tinymce/ja.js +1 -1
  81. package/package.json +3 -3
  82. package/scripts/commitTranslations.sh +2 -2
  83. package/scripts/publish_to_npm.sh +1 -1
  84. package/bin/jira_tickets.sh +0 -14
package/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ 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
+ ## 5.13.1 - 2024-06-03
9
+
10
+ ### Changed
11
+
12
+ - Re-added file verifiers as a stop gap to non-Canvas contexts to allow
13
+ New Quiz item banks to properly share course files
14
+
15
+ ### Fixed
16
+
17
+ - A11y checker tray refusing to close in New Quizzes
18
+ - Find and Replace Tray now translated correctly
19
+
20
+ ## 5.13.0 - 2024-05-14
21
+
22
+ ### Added
23
+
24
+ - Find and Replace Tray
25
+ - Support for Bahasa Indonesia Language and Irish (Gaeilge) Language
26
+ - Support for tools to always be present in toolbar
27
+ - LTI enhancements
28
+
29
+ ### Changed
30
+
31
+ - Limited list of fonts to self-hosted and websafe
32
+ - Preferred HTML editor stored in localstorage
33
+ - Stopped adding aria-hidden to RCE’s parent label
34
+
35
+ ### Fixed
36
+
37
+ - Focus properly restored after closing a11y checker
38
+ - Allow non relative video srcs when editing captions
39
+ - Enhanced user content now translated correctly
40
+
8
41
  ## 5.12.2 - 2024-01-31
9
42
 
10
43
  ### Changed
@@ -36,7 +69,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
36
69
  - Show media captions in New Quizzes
37
70
  - Bump Instui to 8.49
38
71
 
39
-
40
72
  ## 5.11.1 - 2023-10-12
41
73
 
42
74
  ### Fixed
@@ -19,6 +19,7 @@
19
19
  // or a base URL makes testing difficult, esp since window.location is "about:blank"
20
20
  // in mocha tests.
21
21
  import { parse, format } from 'url';
22
+ import RCEGlobals from '../rce/RCEGlobals';
22
23
 
23
24
  function parseCanvasUrl(url) {
24
25
  let canvasOrigin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : window.location.origin;
@@ -95,6 +96,8 @@ export function fixupFileUrl(contextType, contextId, fileInfo, canvasOrigin) {
95
96
  const key = fileInfo.href ? 'href' : 'url';
96
97
 
97
98
  if (fileInfo[key]) {
99
+ var _RCEGlobals$getFeatur;
100
+
98
101
  let parsed = parseCanvasUrl(fileInfo[key], canvasOrigin);
99
102
 
100
103
  if (!parsed) {
@@ -103,8 +106,9 @@ export function fixupFileUrl(contextType, contextId, fileInfo, canvasOrigin) {
103
106
 
104
107
  parsed = changeDownloadToWrapParams(parsed);
105
108
  parsed = addContext(parsed, contextType, contextId); // if this is a user file, add the verifier
109
+ // if this is in New Quizzes and the feature flag is enabled, add the verifier
106
110
 
107
- if (fileInfo.uuid && contextType.includes('user')) {
111
+ if (fileInfo.uuid && (contextType.includes('user') || !!canvasOrigin && canvasOrigin !== window.location.origin && (_RCEGlobals$getFeatur = RCEGlobals.getFeatures()) !== null && _RCEGlobals$getFeatur !== void 0 && _RCEGlobals$getFeatur.file_verifiers_for_quiz_links)) {
108
112
  delete parsed.search;
109
113
  parsed.query.verifier = fileInfo.uuid;
110
114
  } else {
@@ -50,7 +50,8 @@ const defaultTinymceConfig = {
50
50
  content_style: undefined,
51
51
  // this will always be provided by the RCE
52
52
  convert_urls: false,
53
- font_formats: "Lato=lato,Helvetica Neue,Helvetica,Arial,sans-serif; Balsamiq Sans=Balsamiq Sans,lato,Helvetica Neue,Helvetica,Arial,sans-serif; Architect's Daughter=Architects Daughter,lato,Helvetica Neue,Helvetica,Arial,sans-serif; Andale Mono=andale mono,times; Arial=arial,helvetica,sans-serif; Arial Black=arial black,avant garde; Book Antiqua=book antiqua,palatino; Comic Sans MS=comic sans ms,sans-serif; Courier New=courier new,courier; Georgia=georgia,palatino; Helvetica=helvetica; Impact=impact,chicago; Symbol=symbol; Tahoma=tahoma,arial,helvetica,sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva; Webdings=webdings; Wingdings=wingdings,zapf dingbats",
53
+ // fonts specified here need to either be web-safe or self-hosted and loaded in app/stylesheets/bundles/fonts.scss
54
+ font_formats: "Lato=lato,Helvetica Neue,Helvetica,Arial,sans-serif; Balsamiq Sans=Balsamiq Sans,lato,Helvetica Neue,Helvetica,Arial,sans-serif; Architect's Daughter=Architects Daughter,lato,Helvetica Neue,Helvetica,Arial,sans-serif; Arial=arial,helvetica,sans-serif; Arial Black=arial black,avant garde; Courier New=courier new,courier; Georgia=georgia,palatino; Tahoma=tahoma,arial,helvetica,sans-serif; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva",
54
55
  language_load: false,
55
56
  language_url: 'none',
56
57
  toolbar_mode: 'floating',
@@ -22,7 +22,8 @@ import { isExternalLink, showFilePreview, youTubeID } from './instructure_helper
22
22
  import mediaCommentThumbnail from './media_comment_thumbnail';
23
23
  import { addParentFrameContextToUrl } from '../rce/plugins/instructure_rce_external_tools/util/addParentFrameContextToUrl';
24
24
  import { MathJaxDirective, Mathml } from './mathml';
25
- import { makeExternalLinkIcon } from './external_links'; // in jest the es directory doesn't exist so stub the undefined svg
25
+ import { makeExternalLinkIcon } from './external_links';
26
+ import getTranslations from '../getTranslations'; // in jest the es directory doesn't exist so stub the undefined svg
26
27
 
27
28
  const IconDownloadSVG = (IconDownloadLine === null || IconDownloadLine === void 0 ? void 0 : IconDownloadLine.src) || '<svg></svg>';
28
29
 
@@ -110,6 +111,26 @@ function buildUrl(url) {
110
111
  }
111
112
  }
112
113
 
114
+ const addResourceIdentifiersToStudioContent = content => {
115
+ content.querySelectorAll('iframe.lti-embed').forEach(iframe => {
116
+ var _userContentContainer, _userContentContainer2;
117
+
118
+ const url = buildUrl(iframe.getAttribute('src'));
119
+
120
+ if (!url || !url.pathname.includes('external_tools/retrieve') || !url.search.includes('instructuremedia.com') || !url.search.includes('custom_arc_media_id')) {
121
+ return;
122
+ }
123
+
124
+ const userContentContainer = iframe.closest('.user_content');
125
+
126
+ if (userContentContainer !== null && userContentContainer !== void 0 && (_userContentContainer = userContentContainer.dataset) !== null && _userContentContainer !== void 0 && _userContentContainer.resourceType && userContentContainer !== null && userContentContainer !== void 0 && (_userContentContainer2 = userContentContainer.dataset) !== null && _userContentContainer2 !== void 0 && _userContentContainer2.resourceId) {
127
+ url.searchParams.set('com_instructure_course_canvas_resource_type', userContentContainer.dataset.resourceType);
128
+ url.searchParams.set('com_instructure_course_canvas_resource_id', userContentContainer.dataset.resourceId);
129
+ iframe.src = url.href;
130
+ }
131
+ });
132
+ };
133
+
113
134
  export function enhanceUserContent() {
114
135
  let container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
115
136
  let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -132,6 +153,13 @@ export function enhanceUserContent() {
132
153
  */
133
154
  containingCanvasLtiToolId
134
155
  } = opts;
156
+ getTranslations(locale).then(() => {
157
+ formatMessage.setup({
158
+ locale: locale || 'en'
159
+ });
160
+ }).catch(_err => {
161
+ console.error('Failed loading the language file for', locale, '. Falling back to English.');
162
+ });
135
163
  const content = container instanceof HTMLElement && container || document.getElementById('content') || document;
136
164
 
137
165
  const showFilePreviewEx = event => showFilePreview(event, {
@@ -226,6 +254,7 @@ export function enhanceUserContent() {
226
254
  const externalLinkIcon = makeExternalLinkIcon(childLink);
227
255
  childLink.appendChild(externalLinkIcon);
228
256
  });
257
+ addResourceIdentifiersToStudioContent(unenhanced_elem);
229
258
  });
230
259
  content.querySelectorAll('a.instructure_file_link, a.instructure_scribd_file').forEach(file_link => {
231
260
  const href = buildUrl(file_link.href); // Don't attempt to enhance links with no href
@@ -140,6 +140,10 @@ export default function getTranslations(locale) {
140
140
  p = import('./translations/locales/fr_CA');
141
141
  break;
142
142
 
143
+ case 'ga':
144
+ p = import('./translations/locales/ga');
145
+ break;
146
+
143
147
  case 'he':
144
148
  p = import('./translations/locales/he');
145
149
  break;
@@ -336,5 +340,5 @@ export default function getTranslations(locale) {
336
340
  return transReadyPromise;
337
341
  }
338
342
  export function getLocaleList() {
339
- return ['ab', 'ar', 'ca', 'cs', 'cs-CZ', 'cy', 'da', 'da-x-k12', 'da-DK', 'de', 'el', 'en', 'en-AU-x-unimelb', 'en-GB-x-ukhe', 'en-AU', 'en-CA', 'en-CY', 'en-GB', 'en-NZ', 'en-SE', 'en-US', 'es', 'es-ES', 'es-GT', 'fa-IR', 'fi', 'fr', 'fr-CA', 'he', 'ht', 'hu', 'hu-HU', 'hy', 'id', 'id-ID', 'is', 'it', 'ja', 'ko', 'ko-KR', 'lt', 'lt-LT', 'mi', 'mn-MN', 'ms', 'nb', 'nb-x-k12', 'nl', 'nl-NL', 'nn', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'se', 'sl', 'sv', 'sv-x-k12', 'sv-SE', 'tg', 'th', 'th-TH', 'tl-PH', 'tr', 'uk-UA', 'vi', 'vi-VN', 'zh', 'zh-Hans', 'zh-Hant', 'zh-HK', 'zh-TW', 'zh-TW.Big5'];
343
+ return ['ab', 'ar', 'ca', 'cs', 'cs-CZ', 'cy', 'da', 'da-x-k12', 'da-DK', 'de', 'el', 'en', 'en-AU-x-unimelb', 'en-GB-x-ukhe', 'en-AU', 'en-CA', 'en-CY', 'en-GB', 'en-NZ', 'en-SE', 'en-US', 'es', 'es-ES', 'es-GT', 'fa-IR', 'fi', 'fr', 'fr-CA', 'ga', 'he', 'ht', 'hu', 'hu-HU', 'hy', 'id', 'id-ID', 'is', 'it', 'ja', 'ko', 'ko-KR', 'lt', 'lt-LT', 'mi', 'mn-MN', 'ms', 'nb', 'nb-x-k12', 'nl', 'nl-NL', 'nn', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'se', 'sl', 'sv', 'sv-x-k12', 'sv-SE', 'tg', 'th', 'th-TH', 'tl-PH', 'tr', 'uk-UA', 'vi', 'vi-VN', 'zh', 'zh-Hans', 'zh-Hant', 'zh-HK', 'zh-TW', 'zh-TW.Big5'];
340
344
  }
@@ -59,6 +59,7 @@ import { transformRceContentForEditing } from './transformContent';
59
59
  import { IconMoreSolid } from '@instructure/ui-icons/es/svg';
60
60
  import EncryptedStorage from '../util/encrypted-storage';
61
61
  import buildStyle from './style';
62
+ import { externalToolsForToolbar } from './plugins/instructure_rce_external_tools/RceToolWrapper';
62
63
  const RestoreAutoSaveModal = /*#__PURE__*/React.lazy(() => import('./RestoreAutoSaveModal'));
63
64
  const RceHtmlEditor = /*#__PURE__*/React.lazy(() => import('./RceHtmlEditor'));
64
65
  const ASYNC_FOCUS_TIMEOUT = 250;
@@ -139,11 +140,6 @@ export function storageAvailable() {
139
140
  }
140
141
  }
141
142
 
142
- function getHtmlEditorCookie() {
143
- const value = getCookie('rce.htmleditor');
144
- return value === RAW_HTML_EDITOR_VIEW || value === PRETTY_HTML_EDITOR_VIEW ? value : PRETTY_HTML_EDITOR_VIEW;
145
- }
146
-
147
143
  function renderLoading() {
148
144
  return formatMessage('Loading');
149
145
  }
@@ -193,7 +189,9 @@ class RCEWrapper extends React.Component {
193
189
  this.checkAccessibility();
194
190
 
195
191
  if (newView === PRETTY_HTML_EDITOR_VIEW || newView === RAW_HTML_EDITOR_VIEW) {
196
- document.cookie = `rce.htmleditor=${newView};path=/;max-age=31536000`;
192
+ var _this$storage, _this$storage$setItem;
193
+
194
+ (_this$storage = this.storage) === null || _this$storage === void 0 ? void 0 : (_this$storage$setItem = _this$storage.setItem) === null || _this$storage$setItem === void 0 ? void 0 : _this$storage$setItem.call(_this$storage, 'rce.htmleditor', newView);
197
195
  } // Emit view change event
198
196
 
199
197
 
@@ -392,7 +390,19 @@ class RCEWrapper extends React.Component {
392
390
 
393
391
  this.checkAccessibility();
394
392
  this.fixToolbarKeyboardNavigation();
395
- (_this$props$onInitted = (_this$props = this.props).onInitted) === null || _this$props$onInitted === void 0 ? void 0 : _this$props$onInitted.call(_this$props, editor);
393
+ (_this$props$onInitted = (_this$props = this.props).onInitted) === null || _this$props$onInitted === void 0 ? void 0 : _this$props$onInitted.call(_this$props, editor); // cleans up highlight artifacts from findreplace plugin
394
+
395
+ if (this.getRequiredFeatureStatuses().rce_find_replace) {
396
+ editor.on('undo redo', e => {
397
+ var _editor$dom, _editor$dom$doc, _editor$dom$doc$getEl, _editor$dom$doc$getEl2;
398
+
399
+ if ((editor === null || editor === void 0 ? void 0 : (_editor$dom = editor.dom) === null || _editor$dom === void 0 ? void 0 : (_editor$dom$doc = _editor$dom.doc) === null || _editor$dom$doc === void 0 ? void 0 : (_editor$dom$doc$getEl = _editor$dom$doc.getElementsByClassName) === null || _editor$dom$doc$getEl === void 0 ? void 0 : (_editor$dom$doc$getEl2 = _editor$dom$doc$getEl.call(_editor$dom$doc, 'mce-match-marker')) === null || _editor$dom$doc$getEl2 === void 0 ? void 0 : _editor$dom$doc$getEl2.length) > 0) {
400
+ var _editor$plugins, _editor$plugins$searc;
401
+
402
+ (_editor$plugins = editor.plugins) === null || _editor$plugins === void 0 ? void 0 : (_editor$plugins$searc = _editor$plugins.searchreplace) === null || _editor$plugins$searc === void 0 ? void 0 : _editor$plugins$searc.done();
403
+ }
404
+ });
405
+ }
396
406
  };
397
407
 
398
408
  this.fixToolbarKeyboardNavigation = () => {
@@ -623,10 +633,11 @@ class RCEWrapper extends React.Component {
623
633
  }
624
634
  };
625
635
 
626
- this.onA11yChecker = () => {
636
+ this.onA11yChecker = triggerElementId => {
627
637
  const editor = this.mceInstance();
628
638
  editor.execCommand('openAccessibilityChecker', false, {
629
639
  mountNode: instuiPopupMountNode,
640
+ triggerElementId,
630
641
  onFixError: errors => {
631
642
  this.setState({
632
643
  a11yErrorsCount: errors.length
@@ -780,10 +791,11 @@ class RCEWrapper extends React.Component {
780
791
  shouldShowEditor: typeof IntersectionObserver === 'undefined' || maxInitRenderedRCEs <= 0 || currentRCECount < maxInitRenderedRCEs
781
792
  };
782
793
  this._statusBarId = `${this.state.id}_statusbar`;
783
- this.pendingEventHandlers = []; // Get top 2 favorited LTI Tools
784
-
785
- this.ltiToolFavorites = this.props.ltiTools.filter(e => e.favorite).map(e => `instructure_external_button_${e.id}`).slice(0, 2) || [];
794
+ this.pendingEventHandlers = [];
795
+ this.ltiToolFavorites = externalToolsForToolbar(this.props.ltiTools).map(e => `instructure_external_button_${e.id}`);
786
796
  this.pluginsToExclude = parsePluginsToExclude(((_props$editorOptions2 = props.editorOptions) === null || _props$editorOptions2 === void 0 ? void 0 : _props$editorOptions2.plugins) || []);
797
+ this.resourceType = props.resourceType;
798
+ this.resourceId = props.resourceId;
787
799
  this.tinymceInitOptions = this.wrapOptions(props.editorOptions);
788
800
  alertHandler.alertFunc = this.addAlert;
789
801
  this.handleContentTrayClosing = this.handleContentTrayClosing.bind(this);
@@ -821,13 +833,17 @@ class RCEWrapper extends React.Component {
821
833
  new_math_equation_handling = false,
822
834
  explicit_latex_typesetting = false,
823
835
  rce_transform_loaded_content = false,
824
- media_links_use_attachment_id = false
836
+ media_links_use_attachment_id = false,
837
+ rce_find_replace = false,
838
+ file_verifiers_for_quiz_links = false
825
839
  } = this.props.features;
826
840
  return {
827
841
  new_math_equation_handling,
828
842
  explicit_latex_typesetting,
829
843
  rce_transform_loaded_content,
830
- media_links_use_attachment_id
844
+ media_links_use_attachment_id,
845
+ file_verifiers_for_quiz_links,
846
+ rce_find_replace
831
847
  };
832
848
  }
833
849
 
@@ -841,6 +857,13 @@ class RCEWrapper extends React.Component {
841
857
 
842
858
  getCanvasUrl() {
843
859
  return this.props.canvasOrigin;
860
+ }
861
+
862
+ getResourceIdentifiers() {
863
+ return {
864
+ resourceType: this.resourceType,
865
+ resourceId: this.resourceId
866
+ };
844
867
  } // getCode and setCode naming comes from tinyMCE
845
868
  // kind of strange but want to be consistent
846
869
 
@@ -955,6 +978,12 @@ class RCEWrapper extends React.Component {
955
978
  this.contentInserted(element);
956
979
  }
957
980
 
981
+ replaceCode(code) {
982
+ if (code !== "" && window.confirm(formatMessage('Content in the editor will be changed. Press Cancel to keep the original content.'))) {
983
+ this.mceInstance().setContent(code);
984
+ }
985
+ }
986
+
958
987
  insertEmbedCode(code) {
959
988
  const editor = this.mceInstance(); // don't replace selected text, but embed after
960
989
 
@@ -1123,6 +1152,19 @@ class RCEWrapper extends React.Component {
1123
1152
  return this.state.id;
1124
1153
  }
1125
1154
 
1155
+ getHtmlEditorStorage() {
1156
+ var _this$storage2, _this$storage2$getIte, _this$storage2$getIte2;
1157
+
1158
+ const cookieValue = getCookie('rce.htmleditor');
1159
+
1160
+ if (cookieValue) {
1161
+ document.cookie = `rce.htmleditor=${cookieValue};path=/;max-age=0`;
1162
+ }
1163
+
1164
+ const value = cookieValue || ((_this$storage2 = this.storage) === null || _this$storage2 === void 0 ? void 0 : (_this$storage2$getIte = _this$storage2.getItem) === null || _this$storage2$getIte === void 0 ? void 0 : (_this$storage2$getIte2 = _this$storage2$getIte.call(_this$storage2, 'rce.htmleditor')) === null || _this$storage2$getIte2 === void 0 ? void 0 : _this$storage2$getIte2.content);
1165
+ return value === RAW_HTML_EDITOR_VIEW || value === PRETTY_HTML_EDITOR_VIEW ? value : PRETTY_HTML_EDITOR_VIEW;
1166
+ }
1167
+
1126
1168
  _isFullscreen() {
1127
1169
  return !!(this.state.fullscreenState.isTinyFullscreen || document[FS_ELEMENT]);
1128
1170
  }
@@ -1471,6 +1513,11 @@ class RCEWrapper extends React.Component {
1471
1513
  canvasPlugins.push('instructure_fullscreen');
1472
1514
  }
1473
1515
 
1516
+ if (this.getRequiredFeatureStatuses().rce_find_replace) {
1517
+ canvasPlugins.push('searchreplace');
1518
+ canvasPlugins.push('instructure_search_and_replace');
1519
+ }
1520
+
1474
1521
  const possibleNewMenubarItems = this.props.editorOptions.menu ? Object.keys(this.props.editorOptions.menu).join(' ') : undefined;
1475
1522
  const wrappedOpts = { ...defaultTinymceConfig,
1476
1523
  ...options,
@@ -1526,7 +1573,7 @@ class RCEWrapper extends React.Component {
1526
1573
  },
1527
1574
  tools: {
1528
1575
  title: formatMessage('Tools'),
1529
- items: 'instructure_wordcount lti_tools_menuitem'
1576
+ items: 'instructure_wordcount lti_tools_menuitem instructure_search_and_replace'
1530
1577
  },
1531
1578
  view: {
1532
1579
  title: formatMessage('View'),
@@ -1684,26 +1731,20 @@ class RCEWrapper extends React.Component {
1684
1731
  }
1685
1732
 
1686
1733
  setEditorView(view) {
1687
- var _this$getTextarea$lab, _this$getTextarea$lab2, _this$getTextarea$lab3, _this$getTextarea$lab4, _this$_elementRef$cur5, _this$getTextarea$lab5, _this$getTextarea$lab6;
1734
+ var _this$_elementRef$cur5;
1688
1735
 
1689
1736
  switch (view) {
1690
1737
  case RAW_HTML_EDITOR_VIEW:
1691
- this.getTextarea().removeAttribute('aria-hidden');
1692
- (_this$getTextarea$lab = this.getTextarea().labels) === null || _this$getTextarea$lab === void 0 ? void 0 : (_this$getTextarea$lab2 = _this$getTextarea$lab[0]) === null || _this$getTextarea$lab2 === void 0 ? void 0 : _this$getTextarea$lab2.removeAttribute('aria-hidden');
1693
1738
  this.mceInstance().hide();
1694
1739
  break;
1695
1740
 
1696
1741
  case PRETTY_HTML_EDITOR_VIEW:
1697
- this.getTextarea().setAttribute('aria-hidden', true);
1698
- (_this$getTextarea$lab3 = this.getTextarea().labels) === null || _this$getTextarea$lab3 === void 0 ? void 0 : (_this$getTextarea$lab4 = _this$getTextarea$lab3[0]) === null || _this$getTextarea$lab4 === void 0 ? void 0 : _this$getTextarea$lab4.setAttribute('aria-hidden', true);
1699
1742
  this.mceInstance().hide();
1700
1743
  (_this$_elementRef$cur5 = this._elementRef.current.querySelector('.CodeMirror')) === null || _this$_elementRef$cur5 === void 0 ? void 0 : _this$_elementRef$cur5.CodeMirror.setCursor(0, 0);
1701
1744
  break;
1702
1745
 
1703
1746
  case WYSIWYG_VIEW:
1704
1747
  this.setCode(this.textareaValue());
1705
- this.getTextarea().setAttribute('aria-hidden', true);
1706
- (_this$getTextarea$lab5 = this.getTextarea().labels) === null || _this$getTextarea$lab5 === void 0 ? void 0 : (_this$getTextarea$lab6 = _this$getTextarea$lab5[0]) === null || _this$getTextarea$lab6 === void 0 ? void 0 : _this$getTextarea$lab6.setAttribute('aria-hidden', true);
1707
1748
  this.mceInstance().show();
1708
1749
  }
1709
1750
  }
@@ -1809,7 +1850,7 @@ class RCEWrapper extends React.Component {
1809
1850
  path: this.state.path,
1810
1851
  wordCount: this.state.wordCount,
1811
1852
  editorView: this.state.editorView,
1812
- preferredHtmlEditor: getHtmlEditorCookie(),
1853
+ preferredHtmlEditor: this.getHtmlEditorStorage(),
1813
1854
  onResize: this.onResize,
1814
1855
  onKBShortcutModalOpen: this.openKBShortcutModal,
1815
1856
  onA11yChecker: this.onA11yChecker,
@@ -49,6 +49,7 @@ export const ltiToolsPropType = PropTypes.arrayOf(PropTypes.shape({
49
49
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
50
50
  // is this a favorite tool?
51
51
  favorite: PropTypes.bool,
52
+ always_on: PropTypes.bool,
52
53
  name: PropTypes.string,
53
54
  description: PropTypes.string,
54
55
  icon_url: PropTypes.string,
@@ -174,16 +174,17 @@ export default function StatusBar(props) {
174
174
 
175
175
  function renderA11yButton() {
176
176
  const a11y = formatMessage('Accessibility Checker');
177
+ const a11yButtonId = 'rce-a11y-btn';
177
178
  const button = /*#__PURE__*/React.createElement(IconButton, {
178
- "data-btn-id": "rce-a11y-btn",
179
+ "data-btn-id": a11yButtonId,
179
180
  color: "primary",
180
181
  title: a11y,
181
- tabIndex: tabIndexForBtn('rce-a11y-btn'),
182
+ tabIndex: tabIndexForBtn(a11yButtonId),
182
183
  onClick: event => {
183
184
  event.target.focus();
184
- props.onA11yChecker();
185
+ props.onA11yChecker(a11yButtonId);
185
186
  },
186
- onFocus: () => setFocusedBtnId('rce-a11y-btn'),
187
+ onFocus: () => setFocusedBtnId(a11yButtonId),
187
188
  screenReaderLabel: a11y,
188
189
  withBackground: false,
189
190
  withBorder: false
@@ -38,11 +38,13 @@ const mapping = {
38
38
  fi: 'fi',
39
39
  fr: 'fr_FR',
40
40
  'fr-CA': 'fr_FR',
41
+ ga: 'ga',
41
42
  he: 'he_IL',
42
43
  ht: undefined,
43
44
  // tiny doesn't have Haitian Creole
44
45
  hu: 'hu_HU',
45
46
  hy: 'hy',
47
+ id: 'id',
46
48
  is: undefined,
47
49
  // tiny doesn't have Icelandic
48
50
  it: 'it',
@@ -131,7 +131,7 @@ export function fromVideoEmbed($element) {
131
131
 
132
132
  if (RCEGlobals.getFeatures().media_links_use_attachment_id) {
133
133
  const source = $videoIframe.getAttribute('src');
134
- const matches = source === null || source === void 0 ? void 0 : source.match(/^\/media_attachments_iframe\/(\d+)/);
134
+ const matches = source === null || source === void 0 ? void 0 : source.match(/\/media_attachments_iframe\/(\d+)/);
135
135
 
136
136
  if (matches) {
137
137
  videoOptions.attachmentId = matches[1];
@@ -157,6 +157,12 @@ export function externalToolsEnvFor(editor) {
157
157
  var _this$rceWrapper;
158
158
 
159
159
  (_this$rceWrapper = this.rceWrapper) === null || _this$rceWrapper === void 0 ? void 0 : _this$rceWrapper.insertCode(code);
160
+ },
161
+
162
+ replaceCode(code) {
163
+ var _this$rceWrapper2;
164
+
165
+ (_this$rceWrapper2 = this.rceWrapper) === null || _this$rceWrapper2 === void 0 ? void 0 : _this$rceWrapper2.replaceCode(code);
160
166
  }
161
167
 
162
168
  };
@@ -20,10 +20,35 @@ import { simpleCache } from '../../../util/simpleCache';
20
20
  import { instUiIconsArray } from '../../../util/instui-icon-helper'; // @ts-ignore
21
21
 
22
22
  import { IconLtiSolid } from '@instructure/ui-icons/es/svg';
23
-
23
+ export function externalToolsForToolbar(tools) {
24
+ const favorited = tools.filter(it => it.favorite).slice(0, 2) || []; // There's no limit to always on apps, but in practice there shouldn't be more than 2 as well.
25
+
26
+ const alwaysOn = tools.filter(it => it.always_on) || [];
27
+ const set = new Map(); // Remove possible overlaps between favorited and alwaysOn, otherwise
28
+ // we'd have duplicate buttons in the toolbar.
29
+
30
+ for (const toolInfo of favorited.concat(alwaysOn)) {
31
+ set.set(toolInfo.id, toolInfo);
32
+ }
33
+
34
+ return Array.from(set.values()).sort((a, b) => {
35
+ if (a.always_on && !b.always_on) {
36
+ return -1;
37
+ } else if (!a.always_on && b.always_on) {
38
+ return 1;
39
+ } else {
40
+ // This *should* always be a string, but there might be cases where it isn't,
41
+ // especially when this method is used outside of TypeScript files.
42
+ return a.id.toString().localeCompare(b.id.toString(), undefined, {
43
+ numeric: true
44
+ });
45
+ }
46
+ });
47
+ }
24
48
  /**
25
49
  * Helper class for the connection between an external tool registration and a particular TinyMCE instance.
26
50
  */
51
+
27
52
  export class RceToolWrapper {
28
53
  static forEditorEnv(env) {
29
54
  let toolConfigs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : env.availableRceLtiTools;
@@ -82,6 +107,10 @@ export class RceToolWrapper {
82
107
  return this.toolInfo.use_tray;
83
108
  }
84
109
 
110
+ get always_on() {
111
+ return this.toolInfo.always_on;
112
+ }
113
+
85
114
  asToolbarButton() {
86
115
  var _this$iconId;
87
116
 
@@ -1,5 +1,10 @@
1
1
  import _pt from "prop-types";
2
2
 
3
+ /* eslint-disable */
4
+ // @ts-nocheck
5
+ // TODO: we get complaints about <Overlay> because it can be either a Modal or a Tray
6
+ // and they have different props. I don't have time to fix this the right way now.
7
+
3
8
  /*
4
9
  * Copyright (C) 2019 - present Instructure, Inc.
5
10
  *
@@ -197,7 +202,7 @@ export default class ExternalToolDialog extends React.Component {
197
202
  }
198
203
 
199
204
  render() {
200
- var _state$button, _state$button$title, _state$button2, _state$button3, _state$button$width, _state$button4, _state$button5, _state$button$height, _state$button6;
205
+ var _state$button, _props$env$rceWrapper, _props$env$rceWrapper2, _state$button$title, _state$button2, _state$button3, _state$button$width, _state$button4, _state$button5, _state$button$height, _state$button6;
201
206
 
202
207
  const state = this.state;
203
208
  const props = this.props;
@@ -224,6 +229,14 @@ export default class ExternalToolDialog extends React.Component {
224
229
  type: "hidden",
225
230
  name: "editor_contents",
226
231
  value: state.form.contents
232
+ }), /*#__PURE__*/React.createElement("input", {
233
+ type: "hidden",
234
+ name: "com_instructure_course_canvas_resource_type",
235
+ value: (_props$env$rceWrapper = props.env.rceWrapper) === null || _props$env$rceWrapper === void 0 ? void 0 : _props$env$rceWrapper.getResourceIdentifiers().resourceType
236
+ }), /*#__PURE__*/React.createElement("input", {
237
+ type: "hidden",
238
+ name: "com_instructure_course_canvas_resource_id",
239
+ value: (_props$env$rceWrapper2 = props.env.rceWrapper) === null || _props$env$rceWrapper2 === void 0 ? void 0 : _props$env$rceWrapper2.getResourceIdentifiers().resourceId
227
240
  }), state.form.parent_frame_context != null && /*#__PURE__*/React.createElement("input", {
228
241
  type: "hidden",
229
242
  name: "parent_frame_context",
@@ -22,7 +22,7 @@ import { showFlashAlert } from '../../../../common/FlashAlert';
22
22
  import formatMessage from '../../../../format-message';
23
23
  export default function processEditorContentItems(event, env, dialog) {
24
24
  try {
25
- var _event$data, _event$data$content_i, _event$data2, _event$data3;
25
+ var _event$data, _event$data$content_i, _event$data2, _event$data4;
26
26
 
27
27
  const ltiEndpoint = (_event$data = event.data) === null || _event$data === void 0 ? void 0 : _event$data.ltiEndpoint;
28
28
  const selection = env.editorSelection;
@@ -38,7 +38,13 @@ export default function processEditorContentItems(event, env, dialog) {
38
38
  });
39
39
 
40
40
  if (parsedItem != null) {
41
- env.insertCode(parsedItem.toHtmlString());
41
+ var _event$data3;
42
+
43
+ if ((_event$data3 = event.data) !== null && _event$data3 !== void 0 && _event$data3.replaceEditorContents) {
44
+ env.replaceCode(parsedItem.toHtmlString());
45
+ } else {
46
+ env.insertCode(parsedItem.toHtmlString());
47
+ }
42
48
  } else if (!unsupportedItemWarningShown) {
43
49
  var _inputItem$type;
44
50
 
@@ -54,7 +60,7 @@ export default function processEditorContentItems(event, env, dialog) {
54
60
  } // Remove "unsaved changes" warnings and close modal
55
61
 
56
62
 
57
- if ((_event$data3 = event.data) !== null && _event$data3 !== void 0 && _event$data3.content_items) {
63
+ if ((_event$data4 = event.data) !== null && _event$data4 !== void 0 && _event$data4.content_items) {
58
64
  dialog === null || dialog === void 0 ? void 0 : dialog.close();
59
65
  }
60
66
  } catch (e) {
@@ -18,7 +18,7 @@
18
18
  import tinymce from 'tinymce';
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
- import { RceToolWrapper, buildToolMenuItems } from './RceToolWrapper';
21
+ import { RceToolWrapper, buildToolMenuItems, externalToolsForToolbar } from './RceToolWrapper';
22
22
  import formatMessage from '../../../format-message';
23
23
  import { ExternalToolSelectionDialog } from './components/ExternalToolSelectionDialog/ExternalToolSelectionDialog';
24
24
  import { ensureToolDialogContainerElem } from './dialog-helper';
@@ -63,7 +63,8 @@ function registerAppsMenu(editor) {
63
63
 
64
64
 
65
65
  function registerFavoriteAppsToolbarButtons(editor) {
66
- RceToolWrapper.forEditorEnv(externalToolsEnvFor(editor)).filter(it => it.favorite).forEach(toolInfo => editor.ui.registry.addButton(`instructure_external_button_${toolInfo.id}`, toolInfo.asToolbarButton()));
66
+ const allTools = RceToolWrapper.forEditorEnv(externalToolsEnvFor(editor));
67
+ externalToolsForToolbar(allTools).forEach(toolInfo => editor.ui.registry.addButton(`instructure_external_button_${toolInfo.id}`, toolInfo.asToolbarButton()));
67
68
  }
68
69
 
69
70
  function registerAppsToolbarButton(editor) {
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright (C) 2023 - 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
+ import React from 'react';
19
+ import ReactDOM from 'react-dom';
20
+ import FindReplaceController from './components/FindReplaceTrayController';
21
+ import { getSelectionContext } from './getSelectionContext';
22
+ const CONTAINER_ID = 'instructure-find-replace-tray-container';
23
+ export default function (editor, document) {
24
+ var _editor$selection, _editor$selection2;
25
+
26
+ const plugin = editor.plugins.searchreplace;
27
+ const initalSelection = (_editor$selection = editor.selection) === null || _editor$selection === void 0 ? void 0 : _editor$selection.getContent({
28
+ format: 'text'
29
+ });
30
+ if (initalSelection) (_editor$selection2 = editor.selection) === null || _editor$selection2 === void 0 ? void 0 : _editor$selection2.collapse(true);
31
+ let container = document.getElementById(CONTAINER_ID);
32
+
33
+ if (container == null) {
34
+ container = document.createElement('div');
35
+ container.id = CONTAINER_ID;
36
+ document.body.appendChild(container);
37
+ }
38
+
39
+ const handleDismiss = () => {
40
+ if (container) ReactDOM.unmountComponentAtNode(container);
41
+ editor.focus(false);
42
+ };
43
+
44
+ ReactDOM.render( /*#__PURE__*/React.createElement(FindReplaceController, {
45
+ plugin: plugin,
46
+ onDismiss: handleDismiss,
47
+ initialText: initalSelection,
48
+ undoManager: editor.undoManager,
49
+ getSelectionContext: () => {
50
+ const selectedElements = editor.dom.doc.getElementsByClassName('mce-match-marker-selected');
51
+ if (selectedElements.length > 0) return getSelectionContext(selectedElements);
52
+ return ['', ''];
53
+ }
54
+ }), container);
55
+ }