@instructure/canvas-rce 5.15.7 → 7.0.0

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 (93) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/Dockerfile +1 -1
  3. package/es/bridge/Bridge.d.ts +6 -0
  4. package/es/common/browser.js +2 -2
  5. package/es/common/fileUrl.js +13 -3
  6. package/es/defaultTinymceConfig.js +165 -4
  7. package/es/enhance-user-content/enhance_user_content.js +1 -1
  8. package/es/enhance-user-content/instructure_helper.js +7 -3
  9. package/es/rce/RCEGlobals.d.ts +0 -2
  10. package/es/rce/RCEGlobals.js +0 -1
  11. package/es/rce/RCEVariants.d.ts +1 -1
  12. package/es/rce/RCEVariants.js +8 -8
  13. package/es/rce/RCEWrapper.d.ts +0 -2
  14. package/es/rce/RCEWrapper.js +6 -27
  15. package/es/rce/contentRendering.js +3 -2
  16. package/es/rce/plugins/instructure_equation/EquationEditorModal/index.d.ts +1 -1
  17. package/es/rce/plugins/instructure_image/ImageEmbedOptions.js +4 -7
  18. package/es/rce/plugins/instructure_image/ImageOptionsTray/index.js +33 -9
  19. package/es/rce/plugins/instructure_rce_external_tools/RceToolWrapper.d.ts +1 -1
  20. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.js +1 -2
  21. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.d.ts +13 -3
  22. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.js +12 -4
  23. package/es/rce/plugins/instructure_record/mediaTranslations.d.ts +3 -0
  24. package/es/rce/plugins/instructure_record/mediaTranslations.js +4 -1
  25. package/es/rce/plugins/shared/ContentSelection.js +4 -7
  26. package/es/rce/plugins/shared/DimensionsInput/DimensionInput.d.ts +4 -2
  27. package/es/rce/plugins/shared/DimensionsInput/DimensionInput.js +10 -3
  28. package/es/rce/plugins/shared/DimensionsInput/index.d.ts +2 -0
  29. package/es/rce/plugins/shared/DimensionsInput/index.js +9 -5
  30. package/es/rce/plugins/shared/ImageOptionsForm.d.ts +4 -1
  31. package/es/rce/plugins/shared/ImageOptionsForm.js +13 -3
  32. package/es/rce/plugins/shared/Upload/UrlPanel.d.ts +9 -3
  33. package/es/rce/plugins/shared/Upload/UrlPanel.js +13 -4
  34. package/es/rce/plugins/shared/do-fetch-api-effect/parse-link-header.js +1 -1
  35. package/es/rce/plugins/shared/fileTypeUtils.js +1 -1
  36. package/es/rce/plugins/tinymce-a11y-checker/node-checker.js +3 -2
  37. package/es/rce/plugins/tinymce-a11y-checker/plugin.js +50 -52
  38. package/es/rce/plugins/tinymce-a11y-checker/utils/dom.d.ts +6 -0
  39. package/es/rce/plugins/tinymce-a11y-checker/utils/dom.js +15 -0
  40. package/es/rce/plugins/tinymce-a11y-checker/utils/rule-enhancer.d.ts +14 -0
  41. package/es/rce/plugins/tinymce-a11y-checker/utils/rule-enhancer.js +53 -0
  42. package/es/rce/screenreaderOnFormat.d.ts +2 -0
  43. package/es/rce/screenreaderOnFormat.js +109 -0
  44. package/es/rce/style.js +29 -29
  45. package/es/rcs/api.d.ts +4 -1
  46. package/es/rcs/api.js +9 -13
  47. package/es/translations/locales/ar.js +42 -0
  48. package/es/translations/locales/ca.js +42 -0
  49. package/es/translations/locales/cy.js +42 -0
  50. package/es/translations/locales/da-x-k12.js +42 -0
  51. package/es/translations/locales/da.js +42 -0
  52. package/es/translations/locales/de.js +42 -0
  53. package/es/translations/locales/en-AU-x-unimelb.js +42 -0
  54. package/es/translations/locales/en-GB-x-ukhe.js +42 -0
  55. package/es/translations/locales/en.js +54 -0
  56. package/es/translations/locales/en_AU.js +42 -0
  57. package/es/translations/locales/en_CA.js +42 -0
  58. package/es/translations/locales/en_CY.js +42 -0
  59. package/es/translations/locales/en_GB.js +42 -0
  60. package/es/translations/locales/es.js +42 -0
  61. package/es/translations/locales/es_ES.js +42 -0
  62. package/es/translations/locales/fi.js +42 -0
  63. package/es/translations/locales/fr.js +42 -0
  64. package/es/translations/locales/fr_CA.js +42 -0
  65. package/es/translations/locales/ga.js +114 -0
  66. package/es/translations/locales/hi.js +42 -0
  67. package/es/translations/locales/ht.js +42 -0
  68. package/es/translations/locales/id.js +42 -0
  69. package/es/translations/locales/is.js +42 -0
  70. package/es/translations/locales/it.js +42 -0
  71. package/es/translations/locales/ja.js +42 -0
  72. package/es/translations/locales/mi.js +42 -0
  73. package/es/translations/locales/ms.js +42 -0
  74. package/es/translations/locales/nb-x-k12.js +42 -0
  75. package/es/translations/locales/nb.js +42 -0
  76. package/es/translations/locales/nl.js +42 -0
  77. package/es/translations/locales/pl.js +42 -0
  78. package/es/translations/locales/pt.js +42 -0
  79. package/es/translations/locales/pt_BR.js +42 -0
  80. package/es/translations/locales/ru.js +42 -0
  81. package/es/translations/locales/sl.js +42 -0
  82. package/es/translations/locales/sv-x-k12.js +42 -0
  83. package/es/translations/locales/sv.js +42 -0
  84. package/es/translations/locales/th.js +42 -0
  85. package/es/translations/locales/vi.js +42 -0
  86. package/es/translations/locales/zh-Hans.js +42 -0
  87. package/es/translations/locales/zh-Hant.js +42 -0
  88. package/es/translations/locales/zh.js +42 -0
  89. package/es/translations/locales/zh_HK.js +42 -0
  90. package/es/util/loadingPlaceholder.js +4 -3
  91. package/package.json +55 -54
  92. package/coverage/canvas-rce-jest.xml +0 -7028
  93. package/tsconfig.tsbuildinfo +0 -1
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (C) 2019 - present Instructure, Inc.
2
+ * Copyright (C) 2025 - present Instructure, Inc.
3
3
  *
4
4
  * This file is part of Canvas.
5
5
  *
@@ -16,7 +16,7 @@
16
16
  * with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
18
 
19
- import React, { useState, useEffect } from 'react';
19
+ import React, { useState, useEffect, useRef } from 'react';
20
20
  import { bool, func, number, shape, string } from 'prop-types';
21
21
  import { Button, CloseButton } from '@instructure/ui-buttons';
22
22
  import { Heading } from '@instructure/ui-heading';
@@ -60,6 +60,17 @@ export default function ImageOptionsTray(props) {
60
60
  minWidth: MIN_WIDTH,
61
61
  minPercentage: MIN_PERCENTAGE
62
62
  });
63
+ const [altHasError, setAltHasError] = useState(false);
64
+ const [urlHasError, setUrlHasError] = useState(false);
65
+ const [dimensionsHasError, setDimensionsHasError] = useState(false);
66
+ const urlRef = useRef(null);
67
+ const altRef = useRef(null);
68
+ const dimensionsRef = useRef(null);
69
+ useEffect(() => {
70
+ setUrlHasError(showUrlField && url === '');
71
+ setAltHasError(displayAs === 'embed' && !isDecorativeImage && altText === '');
72
+ setDimensionsHasError(imageSize === CUSTOM && !dimensionsState?.isValid);
73
+ }, [isDecorativeImage, altText, url, displayAs, showUrlField, imageSize, dimensionsState?.isValid]);
63
74
  function handleUrlChange(newUrl) {
64
75
  setUrl(newUrl);
65
76
  }
@@ -72,7 +83,7 @@ export default function ImageOptionsTray(props) {
72
83
  function handleDisplayAsChange(event) {
73
84
  setDisplayAs(event.target.value);
74
85
  }
75
- function handleImageSizeChange(event, selectedOption) {
86
+ function handleImageSizeChange(_e, selectedOption) {
76
87
  setImageSize(selectedOption.value);
77
88
  if (selectedOption.value === CUSTOM) {
78
89
  setImageHeight(currentHeight);
@@ -88,6 +99,18 @@ export default function ImageOptionsTray(props) {
88
99
  }
89
100
  function handleSave(event) {
90
101
  event.preventDefault();
102
+ if (urlHasError || altHasError || dimensionsHasError) {
103
+ if (altHasError) {
104
+ altRef?.current?.focus();
105
+ }
106
+ if (urlHasError) {
107
+ urlRef?.current?.focus();
108
+ }
109
+ if (dimensionsHasError) {
110
+ dimensionsRef?.current?.focus();
111
+ }
112
+ return;
113
+ }
91
114
  const savedAltText = isDecorativeImage ? '' : altText;
92
115
  let appliedHeight = imageHeight;
93
116
  let appliedWidth = imageWidth;
@@ -134,9 +157,6 @@ export default function ImageOptionsTray(props) {
134
157
  type: 'hint'
135
158
  });
136
159
  }
137
- const disableForIcons = isIconMaker && !isDecorativeImage && altText === '';
138
- const disableForImages = url === '' || displayAs === 'embed' && (!isDecorativeImage && altText === '' || imageSize === CUSTOM && !dimensionsState?.isValid);
139
- const saveDisabled = isIconMaker ? disableForIcons : disableForImages;
140
160
  const trayLabel = isIconMaker ? formatMessage('Icon Options Tray') : formatMessage('Image Options Tray');
141
161
  const trayHeading = isIconMaker ? formatMessage('Icon Options') : formatMessage('Image Options');
142
162
  return /*#__PURE__*/React.createElement(Tray, {
@@ -183,7 +203,9 @@ export default function ImageOptionsTray(props) {
183
203
  padding: "small"
184
204
  }, /*#__PURE__*/React.createElement(UrlPanel, {
185
205
  fileUrl: url,
186
- setFileUrl: handleUrlChange
206
+ urlRef: urlRef,
207
+ setFileUrl: handleUrlChange,
208
+ urlHasError: urlHasError
187
209
  })), /*#__PURE__*/React.createElement(ImageOptionsForm, {
188
210
  id: "image-options-form",
189
211
  imageSize: imageSize,
@@ -197,14 +219,16 @@ export default function ImageOptionsTray(props) {
197
219
  handleDisplayAsChange: handleDisplayAsChange,
198
220
  handleImageSizeChange: handleImageSizeChange,
199
221
  messagesForSize: messagesForSize,
200
- isIconMaker: isIconMaker
222
+ isIconMaker: isIconMaker,
223
+ altHasError: altHasError,
224
+ altRef: altRef,
225
+ dimensionsRef: dimensionsRef
201
226
  })), /*#__PURE__*/React.createElement(Flex.Item, {
202
227
  background: "secondary",
203
228
  borderWidth: "small none none none",
204
229
  padding: "small medium",
205
230
  textAlign: "end"
206
231
  }, /*#__PURE__*/React.createElement(Button, {
207
- disabled: saveDisabled,
208
232
  onClick: handleSave,
209
233
  color: "primary"
210
234
  }, formatMessage('Done')))))));
@@ -33,7 +33,7 @@ export declare class RceToolWrapper {
33
33
  width: import("prop-types").Requireable<number>;
34
34
  use_tray: import("prop-types").Requireable<boolean>;
35
35
  canvas_icon_class: import("prop-types").Requireable<any>;
36
- }, "description" | "id" | "name" | "height" | "width" | "favorite" | "on_by_default" | "icon_url" | "use_tray" | "canvas_icon_class">>>)[], mruIds?: string[]): RceToolWrapper[];
36
+ }, "id" | "name" | "width" | "height" | "description" | "favorite" | "on_by_default" | "icon_url" | "use_tray" | "canvas_icon_class">>>)[], mruIds?: string[]): RceToolWrapper[];
37
37
  readonly iconId: string | null | undefined;
38
38
  isMruTool: boolean;
39
39
  get editor(): import("../../types").ExternalToolsEditor | null;
@@ -21,7 +21,6 @@ import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
22
  import { asAudioElement, findMediaPlayerIframe } from '../../shared/ContentSelection';
23
23
  import AudioOptionsTray from '.';
24
- import RCEGlobals from '../../../RCEGlobals';
25
24
  export const CONTAINER_ID = 'instructure-audio-options-tray-container';
26
25
  export default class TrayController {
27
26
  constructor() {
@@ -74,7 +73,7 @@ export default class TrayController {
74
73
  return elem.parentNode.removeChild(elem);
75
74
  }
76
75
  _applyAudioOptions(audioOptions) {
77
- const hasAttachmentId = RCEGlobals.getFeatures().media_links_use_attachment_id && audioOptions.attachment_id;
76
+ const hasAttachmentId = audioOptions.attachment_id;
78
77
  if (!hasAttachmentId && (!audioOptions.media_object_id || audioOptions.media_object_id === 'undefined')) {
79
78
  return;
80
79
  }
@@ -3,12 +3,22 @@ export namespace VIDEO_SIZE_DEFAULT {
3
3
  let height: string;
4
4
  let width: string;
5
5
  }
6
- export namespace AUDIO_PLAYER_SIZE {
7
- let width_1: string;
8
- export { width_1 as width };
6
+ export namespace STUDIO_PLAYER_VIDEO_SIZE_DEFAULT {
9
7
  let height_1: string;
10
8
  export { height_1 as height };
9
+ let width_1: string;
10
+ export { width_1 as width };
11
+ }
12
+ export namespace AUDIO_PLAYER_SIZE {
13
+ let width_2: string;
14
+ export { width_2 as width };
15
+ let height_2: string;
16
+ export { height_2 as height };
11
17
  }
18
+ export function videoDefaultSize(): {
19
+ height: string;
20
+ width: string;
21
+ };
12
22
  export default class TrayController {
13
23
  _editor: any;
14
24
  _isOpen: boolean;
@@ -28,10 +28,20 @@ export const VIDEO_SIZE_DEFAULT = {
28
28
  height: '225px',
29
29
  width: '400px'
30
30
  }; // AKA "LARGE"
31
+ export const STUDIO_PLAYER_VIDEO_SIZE_DEFAULT = {
32
+ height: '300px',
33
+ width: '480px'
34
+ };
31
35
  export const AUDIO_PLAYER_SIZE = {
32
36
  width: '320px',
33
37
  height: '14.25rem'
34
38
  };
39
+ export const videoDefaultSize = () => {
40
+ if (RCEGlobals.getFeatures().consolidated_media_player) {
41
+ return STUDIO_PLAYER_VIDEO_SIZE_DEFAULT;
42
+ }
43
+ return VIDEO_SIZE_DEFAULT;
44
+ };
35
45
  export default class TrayController {
36
46
  constructor() {
37
47
  this._editor = null;
@@ -108,11 +118,9 @@ export default class TrayController {
108
118
  const data = {
109
119
  media_object_id: videoOptions.media_object_id,
110
120
  title: videoOptions.titleText,
111
- subtitles: videoOptions.subtitles
121
+ subtitles: videoOptions.subtitles,
122
+ attachment_id: videoOptions.attachment_id
112
123
  };
113
- if (RCEGlobals.getFeatures().media_links_use_attachment_id) {
114
- data.attachment_id = videoOptions.attachment_id;
115
- }
116
124
 
117
125
  // If the video just edited came from a file uploaded to canvas
118
126
  // and not notorious, we probably don't have a media_object_id.
@@ -25,6 +25,9 @@ declare namespace uploadMediaTranslations {
25
25
  let ADDED_CAPTION: string;
26
26
  let DELETED_CAPTION: string;
27
27
  let PROGRESS_LABEL: string;
28
+ let SELECT_SUPPORTED_FILE_TYPE: string;
29
+ let CHOOSE_FILE_TO_UPLOAD: string;
30
+ let ENTER_FILE_NAME: string;
28
31
  }
29
32
  namespace SelectStrings {
30
33
  let USE_ARROWS: string;
@@ -46,7 +46,10 @@ const uploadMediaTranslations = {
46
46
  ADD_NEW_CAPTION_OR_SUBTITLE: formatMessage('Add another'),
47
47
  ADDED_CAPTION: 'Captions for {lang} added',
48
48
  DELETED_CAPTION: 'Deleted captions for {lang}',
49
- PROGRESS_LABEL: formatMessage('Uploading')
49
+ PROGRESS_LABEL: formatMessage('Uploading'),
50
+ SELECT_SUPPORTED_FILE_TYPE: formatMessage('Please select a file of a supported type'),
51
+ CHOOSE_FILE_TO_UPLOAD: formatMessage('Please choose a file'),
52
+ ENTER_FILE_NAME: formatMessage('Please enter a file name')
50
53
  },
51
54
  SelectStrings: {
52
55
  USE_ARROWS: 'Use arrow keys to navigate options.',
@@ -20,7 +20,6 @@ import { fromImageEmbed, fromVideoEmbed } from '../instructure_image/ImageEmbedO
20
20
  import { isOnlyTextSelected } from '../../contentInsertionUtils';
21
21
  import formatMessage from '../../../format-message';
22
22
  import { isStudioEmbeddedMedia } from './StudioLtiSupportUtils';
23
- import RCEGlobals from '../../RCEGlobals';
24
23
  import { parseUrlPath } from '../../../util/url-util';
25
24
  const FILE_DOWNLOAD_PATH_REGEX = /^\/(courses\/\d+\/)?files\/\d+\/download$/;
26
25
  export const LINK_TYPE = 'link';
@@ -124,12 +123,10 @@ export function asAudioElement($element) {
124
123
  // eslint-disable-next-line no-empty
125
124
  } catch (e) {}
126
125
  }
127
- if (RCEGlobals.getFeatures().media_links_use_attachment_id) {
128
- const source = $audioIframe.getAttribute('src');
129
- const matches = source?.match(/\/media_attachments_iframe\/(\d+)/);
130
- if (matches) {
131
- audioOptions.attachmentId = matches[1];
132
- }
126
+ const source = $audioIframe.getAttribute('src');
127
+ const matches = source?.match(/\/media_attachments_iframe\/(\d+)/);
128
+ if (matches) {
129
+ audioOptions.attachmentId = matches[1];
133
130
  }
134
131
  return audioOptions;
135
132
  }
@@ -1,13 +1,15 @@
1
1
  declare function DimensionInput(props: any): React.JSX.Element;
2
2
  declare namespace DimensionInput {
3
3
  namespace propTypes {
4
- let dimensionState: import("prop-types").Requireable<import("prop-types").InferProps<{
4
+ export let dimensionState: import("prop-types").Requireable<import("prop-types").InferProps<{
5
5
  addOffset: import("prop-types").Validator<(...args: any[]) => any>;
6
6
  inputValue: import("prop-types").Validator<string>;
7
7
  setInputValue: import("prop-types").Validator<(...args: any[]) => any>;
8
8
  }>>;
9
- let label: import("prop-types").Validator<string>;
9
+ export { object as dimensionsRef };
10
+ export let label: import("prop-types").Validator<string>;
10
11
  }
11
12
  }
12
13
  export default DimensionInput;
13
14
  import React from 'react';
15
+ import { object } from 'prop-types';
@@ -17,14 +17,15 @@
17
17
  */
18
18
 
19
19
  import React from 'react';
20
- import { func, shape, string } from 'prop-types';
20
+ import { func, shape, string, object } from 'prop-types';
21
21
  import { ScreenReaderContent } from '@instructure/ui-a11y-content';
22
22
  import { NumberInput } from '@instructure/ui-number-input';
23
23
  export default function DimensionInput(props) {
24
24
  const {
25
25
  dimensionState,
26
26
  label,
27
- messages
27
+ messages,
28
+ dimensionsRef
28
29
  } = props;
29
30
  const {
30
31
  addOffset,
@@ -49,7 +50,12 @@ export default function DimensionInput(props) {
49
50
  isRequired: true,
50
51
  showArrows: false,
51
52
  value: inputValue,
52
- messages: messages
53
+ messages: messages,
54
+ inputRef: ref => {
55
+ if (dimensionsRef) {
56
+ dimensionsRef.current = ref;
57
+ }
58
+ }
53
59
  });
54
60
  }
55
61
  DimensionInput.propTypes = {
@@ -58,5 +64,6 @@ DimensionInput.propTypes = {
58
64
  inputValue: string.isRequired,
59
65
  setInputValue: func.isRequired
60
66
  }),
67
+ dimensionsRef: object,
61
68
  label: string.isRequired
62
69
  };
@@ -25,6 +25,7 @@ declare namespace DimensionsInput {
25
25
  export let minWidth: import("prop-types").Validator<number>;
26
26
  export let minPercentage: import("prop-types").Validator<number>;
27
27
  export { bool as hidePercentage };
28
+ export { object as dimensionsRef };
28
29
  }
29
30
  namespace defaultProps {
30
31
  let hidePercentage: boolean;
@@ -34,3 +35,4 @@ export default DimensionsInput;
34
35
  export { default as useDimensionsState } from "./useDimensionsState";
35
36
  import React from 'react';
36
37
  import { bool } from 'prop-types';
38
+ import { object } from 'prop-types';
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import React from 'react';
20
- import { bool, func, number, shape, string } from 'prop-types';
20
+ import { bool, func, number, shape, string, object } from 'prop-types';
21
21
  import { ScreenReaderContent } from '@instructure/ui-a11y-content';
22
22
  import { FormFieldGroup } from '@instructure/ui-form-field';
23
23
  import { IconLockLine, IconWarningSolid } from '@instructure/ui-icons';
@@ -79,7 +79,8 @@ export default function DimensionsInput(props) {
79
79
  minHeight,
80
80
  minWidth,
81
81
  minPercentage,
82
- hidePercentage
82
+ hidePercentage,
83
+ dimensionsRef
83
84
  } = props;
84
85
  const handleDimensionTypeChange = e => {
85
86
  dimensionsState.setUsePercentageUnits(e.target.value === 'percentage');
@@ -131,7 +132,8 @@ export default function DimensionsInput(props) {
131
132
  }, /*#__PURE__*/React.createElement(DimensionInput, {
132
133
  dimensionState: dimensionsState.percentageState,
133
134
  label: formatMessage('Percentage'),
134
- messages: [secondaryMessage]
135
+ messages: [secondaryMessage],
136
+ dimensionsRef: dimensionsRef
135
137
  })), /*#__PURE__*/React.createElement(Flex.Item, {
136
138
  padding: "x-small small"
137
139
  }, "%")) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Flex.Item, {
@@ -140,7 +142,8 @@ export default function DimensionsInput(props) {
140
142
  dimensionState: dimensionsState.widthState,
141
143
  label: formatMessage('Width'),
142
144
  minValue: minWidth,
143
- messages: [secondaryMessage]
145
+ messages: [secondaryMessage],
146
+ dimensionsRef: dimensionsRef
144
147
  })), /*#__PURE__*/React.createElement(Flex.Item, {
145
148
  padding: "x-small small"
146
149
  }, /*#__PURE__*/React.createElement(IconLockLine, null)), /*#__PURE__*/React.createElement(Flex.Item, {
@@ -176,7 +179,8 @@ DimensionsInput.propTypes = {
176
179
  minHeight: number.isRequired,
177
180
  minWidth: number.isRequired,
178
181
  minPercentage: number.isRequired,
179
- hidePercentage: bool
182
+ hidePercentage: bool,
183
+ dimensionsRef: object
180
184
  };
181
185
  DimensionsInput.defaultProps = {
182
186
  hidePercentage: false
@@ -1,5 +1,5 @@
1
1
  export default ImageOptionsForm;
2
- declare function ImageOptionsForm({ imageSize, displayAs, isDecorativeImage, altText, isLinked, dimensionsState, handleAltTextChange, handleIsDecorativeChange, handleDisplayAsChange, handleImageSizeChange, messagesForSize, hideDimensions, id, isIconMaker, forBlockEditorUse, }: {
2
+ declare function ImageOptionsForm({ imageSize, displayAs, isDecorativeImage, altText, isLinked, dimensionsState, handleAltTextChange, handleIsDecorativeChange, handleDisplayAsChange, handleImageSizeChange, messagesForSize, hideDimensions, id, isIconMaker, forBlockEditorUse, altHasError, altRef, dimensionsRef }: {
3
3
  imageSize: any;
4
4
  displayAs: any;
5
5
  isDecorativeImage: any;
@@ -15,5 +15,8 @@ declare function ImageOptionsForm({ imageSize, displayAs, isDecorativeImage, alt
15
15
  id?: string | undefined;
16
16
  isIconMaker?: boolean | undefined;
17
17
  forBlockEditorUse?: boolean | undefined;
18
+ altHasError?: boolean | undefined;
19
+ altRef?: null | undefined;
20
+ dimensionsRef?: null | undefined;
18
21
  }): React.JSX.Element;
19
22
  import React from 'react';
@@ -44,7 +44,10 @@ const ImageOptionsForm = ({
44
44
  hideDimensions,
45
45
  id = 'image-options-form',
46
46
  isIconMaker = false,
47
- forBlockEditorUse = false
47
+ forBlockEditorUse = false,
48
+ altHasError = false,
49
+ altRef = null,
50
+ dimensionsRef = null
48
51
  }) => {
49
52
  const TYPE = isIconMaker ? formatMessage('icon') : formatMessage('image');
50
53
  const tooltipText = formatMessage('Used by screen readers to describe the content of an {TYPE}', {
@@ -76,6 +79,7 @@ const ImageOptionsForm = ({
76
79
  }, /*#__PURE__*/React.createElement(Flex.Item, {
77
80
  padding: "small"
78
81
  }, /*#__PURE__*/React.createElement(TextArea, {
82
+ "data-testid": "alt-text-field",
79
83
  disabled: isDecorativeImage,
80
84
  "aria-describedby": "alt-text-label-tooltip",
81
85
  height: "4rem",
@@ -85,7 +89,12 @@ const ImageOptionsForm = ({
85
89
  TYPE
86
90
  }),
87
91
  resize: "vertical",
88
- value: altText
92
+ value: altText,
93
+ messages: altHasError ? [{
94
+ text: formatMessage('Invalid description'),
95
+ type: 'error'
96
+ }] : [],
97
+ ref: altRef
89
98
  })), /*#__PURE__*/React.createElement(Flex.Item, {
90
99
  padding: "small"
91
100
  }, /*#__PURE__*/React.createElement(Checkbox, {
@@ -133,7 +142,8 @@ const ImageOptionsForm = ({
133
142
  disabled: displayAs !== 'embed',
134
143
  minHeight: MIN_HEIGHT,
135
144
  minWidth: MIN_WIDTH,
136
- minPercentage: MIN_PERCENTAGE
145
+ minPercentage: MIN_PERCENTAGE,
146
+ dimensionsRef: dimensionsRef
137
147
  })))));
138
148
  };
139
149
  export default ImageOptionsForm;
@@ -1,12 +1,18 @@
1
- declare function UrlPanel({ fileUrl, setFileUrl }: {
1
+ declare function UrlPanel({ fileUrl, setFileUrl, urlHasError, urlRef }: {
2
2
  fileUrl: any;
3
3
  setFileUrl: any;
4
+ urlHasError: any;
5
+ urlRef: any;
4
6
  }): React.JSX.Element;
5
7
  declare namespace UrlPanel {
6
8
  namespace propTypes {
7
- let fileUrl: import("prop-types").Validator<string>;
8
- let setFileUrl: import("prop-types").Validator<(...args: any[]) => any>;
9
+ export let fileUrl: import("prop-types").Validator<string>;
10
+ export let setFileUrl: import("prop-types").Validator<(...args: any[]) => any>;
11
+ export { bool as urlHasError };
12
+ export { object as urlRef };
9
13
  }
10
14
  }
11
15
  export default UrlPanel;
12
16
  import React from 'react';
17
+ import { bool } from 'prop-types';
18
+ import { object } from 'prop-types';
@@ -17,22 +17,31 @@
17
17
  */
18
18
 
19
19
  import React from 'react';
20
- import { string, func } from 'prop-types';
20
+ import { string, func, bool, object } from 'prop-types';
21
21
  import { TextInput } from '@instructure/ui-text-input';
22
22
  import formatMessage from '../../../../format-message';
23
23
  export default function UrlPanel({
24
24
  fileUrl,
25
- setFileUrl
25
+ setFileUrl,
26
+ urlHasError,
27
+ urlRef
26
28
  }) {
27
29
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, {
28
30
  name: "file-url",
29
31
  renderLabel: formatMessage('File URL'),
30
32
  type: "url",
31
33
  value: fileUrl,
32
- onChange: (e, val) => setFileUrl(val)
34
+ onChange: (_e, val) => setFileUrl(val),
35
+ messages: urlHasError ? [{
36
+ text: formatMessage('Invalid URL'),
37
+ type: 'error'
38
+ }] : [],
39
+ ref: urlRef
33
40
  }));
34
41
  }
35
42
  UrlPanel.propTypes = {
36
43
  fileUrl: string.isRequired,
37
- setFileUrl: func.isRequired
44
+ setFileUrl: func.isRequired,
45
+ urlHasError: bool,
46
+ urlRef: object
38
47
  };
@@ -83,7 +83,7 @@ function intoRels(acc, x) {
83
83
  });
84
84
  return acc;
85
85
  }
86
- const PARSE_LINK_HEADER_MAXLEN = 2000;
86
+ const PARSE_LINK_HEADER_MAXLEN = 4000;
87
87
  const PARSE_LINK_HEADER_THROW_ON_MAXLEN_EXCEEDED = process.env.PARSE_LINK_HEADER_THROW_ON_MAXLEN_EXCEEDED != null;
88
88
  function checkHeader(linkHeader) {
89
89
  if (!linkHeader) return false;
@@ -83,7 +83,7 @@ export function mediaPlayerURLFromFile(file, canvasOrigin) {
83
83
  if (typeof content_type !== 'string') throw new Error('Invalid content type');
84
84
  const type = content_type.replace(/\/.*$/, '');
85
85
  const baseOrigin = canvasOrigin !== null && canvasOrigin !== void 0 ? canvasOrigin : window.location.origin;
86
- if (RCEGlobals.getFeatures()?.media_links_use_attachment_id && isAudioOrVideo(content_type) && file.id) {
86
+ if (isAudioOrVideo(content_type) && file.id) {
87
87
  const url = new URL(`/media_attachments_iframe/${file.id}`, baseOrigin);
88
88
  url.searchParams.set('type', type);
89
89
  url.searchParams.set('embedded', 'true');
@@ -18,6 +18,7 @@
18
18
 
19
19
  import * as dom from './utils/dom';
20
20
  import rules from './rules';
21
+ import { enhanceRules } from './utils/rule-enhancer';
21
22
  export default function checkNode(node, done, config = {}, additionalRules = []) {
22
23
  if (!node) {
23
24
  return;
@@ -25,9 +26,9 @@ export default function checkNode(node, done, config = {}, additionalRules = [])
25
26
  const errors = [];
26
27
  const childNodeCheck = child => {
27
28
  if (child.hasAttribute('data-ignore-a11y-check')) return;
28
- const composedRules = rules.concat(additionalRules);
29
+ // Enhance all rules with TinyMCE notification functionality
30
+ const composedRules = enhanceRules(rules.concat(additionalRules));
29
31
  for (const rule of composedRules) {
30
- // eslint-disable-next-line promise/catch-or-return
31
32
  Promise.resolve(rule.test(child, config)).then(result => {
32
33
  if (!result) {
33
34
  errors.push({
@@ -27,60 +27,58 @@ const pendingInstanceCallbacks = [];
27
27
  const container = document.createElement('div');
28
28
  container.className = 'tinymce-a11y-checker-container';
29
29
  document.body.appendChild(container);
30
- tinymce.create('tinymce.plugins.AccessibilityChecker', {
31
- init(ed) {
32
- ed.addCommand('openAccessibilityChecker', function (ui, {
33
- done,
34
- config,
35
- additionalRules,
36
- mountNode,
37
- triggerElementId,
38
- onFixError
39
- }) {
40
- if (!isCheckerOpen) {
41
- ReactDOM.render(/*#__PURE__*/React.createElement(Checker, {
42
- getBody: ed.getBody.bind(ed),
43
- editor: ed,
44
- additionalRules: additionalRules,
45
- mountNode: mountNode,
46
- onClose: () => {
47
- isCheckerOpen = false;
48
- if (triggerElementId) {
49
- const button = document.querySelectorAll(`[data-btn-id=${triggerElementId}]`);
50
- button[0]?.focus();
51
- }
52
- },
53
- onFixError: onFixError
54
- }), container, function () {
55
- // this is a workaround for react 16 since ReactDOM.render is not
56
- // guaranteed to return the instance synchronously (especially if called
57
- // within another component's lifecycle method eg: componentDidMount). see:
58
- // https://github.com/facebook/react/issues/10309#issuecomment-318434635
59
- instance = this;
60
- if (config) getInstance(instance => instance.setConfig(config));
61
- pendingInstanceCallbacks.forEach(cb => cb(instance));
62
- instance.check(done);
63
- });
64
- isCheckerOpen = true;
65
- }
66
- });
67
- ed.addCommand('checkAccessibility', function (ui, {
68
- done,
69
- config,
70
- additionalRules
71
- }) {
72
- checkNode(ed.getBody(), done, config, additionalRules);
73
- });
74
- ed.ui.registry.addButton('check_a11y', {
75
- title: formatMessage('Check Accessibility'),
76
- onAction: _ => ed.execCommand('openAccessibilityChecker'),
77
- icon: 'a11y'
78
- });
79
- }
80
- });
30
+ const AccessibilityChecker = function (ed) {
31
+ ed.addCommand('openAccessibilityChecker', function (ui, {
32
+ done,
33
+ config,
34
+ additionalRules,
35
+ mountNode,
36
+ triggerElementId,
37
+ onFixError
38
+ }) {
39
+ if (!isCheckerOpen) {
40
+ ReactDOM.render(/*#__PURE__*/React.createElement(Checker, {
41
+ getBody: ed.getBody.bind(ed),
42
+ editor: ed,
43
+ additionalRules: additionalRules,
44
+ mountNode: mountNode,
45
+ onClose: () => {
46
+ isCheckerOpen = false;
47
+ if (triggerElementId) {
48
+ const button = document.querySelectorAll(`[data-btn-id=${triggerElementId}]`);
49
+ button[0]?.focus();
50
+ }
51
+ },
52
+ onFixError: onFixError
53
+ }), container, function () {
54
+ // this is a workaround for react 16 since ReactDOM.render is not
55
+ // guaranteed to return the instance synchronously (especially if called
56
+ // within another component's lifecycle method eg: componentDidMount). see:
57
+ // https://github.com/facebook/react/issues/10309#issuecomment-318434635
58
+ instance = this;
59
+ if (config) getInstance(instance => instance.setConfig(config));
60
+ pendingInstanceCallbacks.forEach(cb => cb(instance));
61
+ instance.check(done);
62
+ });
63
+ isCheckerOpen = true;
64
+ }
65
+ });
66
+ ed.addCommand('checkAccessibility', function (ui, {
67
+ done,
68
+ config,
69
+ additionalRules
70
+ }) {
71
+ checkNode(ed.getBody(), done, config, additionalRules);
72
+ });
73
+ ed.ui.registry.addButton('check_a11y', {
74
+ title: formatMessage('Check Accessibility'),
75
+ onAction: _ => ed.execCommand('openAccessibilityChecker'),
76
+ icon: 'a11y'
77
+ });
78
+ };
81
79
 
82
80
  // Register plugin
83
- tinymce.PluginManager.add('a11y_checker', tinymce.plugins.AccessibilityChecker);
81
+ tinymce.PluginManager.add('a11y_checker', AccessibilityChecker);
84
82
  export function getInstance(cb) {
85
83
  if (instance != null) {
86
84
  return cb(instance);
@@ -8,4 +8,10 @@ export function onlyContainsLink(elem: any): boolean;
8
8
  export function splitStyleAttribute(styleString: any): any;
9
9
  export function createStyleString(styleObj: any): string;
10
10
  export function hasTextNode(elem: any): boolean;
11
+ /**
12
+ * Notifies TinyMCE that a change has been made
13
+ * This ensures that changes persist in the editor's state without requiring additional user actions
14
+ * @returns {void}
15
+ */
16
+ export function notifyTinyMCE(): void;
11
17
  import indicate from './indicate';