@instructure/canvas-rce 7.3.1 → 8.1.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 (155) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/{es/rce/plugins/shared/ai_tools/index.js → __mocks__/@instructure/ui-media-player/_mockUiMediaPlayer.js} +4 -4
  3. package/__tests__/common/mimeClass.test.js +25 -1
  4. package/__tests__/rcs/api.test.js +280 -251
  5. package/es/canvasFileBrowser/FileBrowser.d.ts +2 -2
  6. package/es/canvasFileBrowser/FileBrowser.js +8 -7
  7. package/es/common/mimeClass.js +3 -1
  8. package/es/defaultTinymceConfig.js +47 -49
  9. package/es/enhance-user-content/enhance_user_content.js +6 -8
  10. package/es/enhance-user-content/index.d.ts +3 -1
  11. package/es/enhance-user-content/index.js +3 -1
  12. package/es/enhance-user-content/youtube_overlay.js +18 -0
  13. package/es/getThemeVars.d.ts +1 -1
  14. package/es/getThemeVars.js +23 -26
  15. package/es/rce/KeyboardShortcutModal.js +1 -1
  16. package/es/rce/RCE.d.ts +9 -0
  17. package/es/rce/RCE.js +4 -0
  18. package/es/rce/RCEGlobals.d.ts +2 -0
  19. package/es/rce/RCEGlobals.js +1 -0
  20. package/es/rce/RCEVariants.d.ts +1 -2
  21. package/es/rce/RCEVariants.js +1 -2
  22. package/es/rce/RCEWrapper.d.ts +6 -16
  23. package/es/rce/RCEWrapper.js +18 -87
  24. package/es/rce/RCEWrapper.utils.d.ts +1 -1
  25. package/es/rce/RCEWrapperProps.d.ts +2 -1
  26. package/es/rce/RCEWrapperProps.js +2 -1
  27. package/es/rce/StatusBar.d.ts +0 -1
  28. package/es/rce/StatusBar.js +3 -28
  29. package/es/rce/plugins/instructure_equation/EquationEditorModal/advancedOnlySyntax.d.ts +2 -1
  30. package/es/rce/plugins/instructure_equation/EquationEditorModal/advancedOnlySyntax.js +3 -1
  31. package/es/rce/plugins/instructure_equation/EquationEditorModal/index.d.ts +1 -0
  32. package/es/rce/plugins/instructure_equation/EquationEditorModal/index.js +12 -2
  33. package/es/rce/plugins/instructure_icon_maker/components/CreateIconMakerForm/ImageSection/ImageOptions.js +2 -2
  34. package/es/rce/plugins/instructure_icon_maker/components/CreateIconMakerForm/ImageSection/ImageSection.js +3 -3
  35. package/es/rce/plugins/instructure_icon_maker/svg/constants.d.ts +20 -5
  36. package/es/rce/plugins/instructure_icon_maker/svg/utils.d.ts +1 -1
  37. package/es/rce/plugins/instructure_icon_maker/utils/IconMakerFormHasChanges.js +2 -2
  38. package/es/rce/plugins/instructure_image/ImageEmbedOptions.d.ts +0 -2
  39. package/es/rce/plugins/instructure_image/ImageEmbedOptions.js +2 -9
  40. package/es/rce/plugins/instructure_paste/plugin.js +18 -12
  41. package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolDialog/ExternalToolDialogModal.d.ts +1 -1
  42. package/es/rce/plugins/instructure_rce_external_tools/components/util/ToolLaunchIframe.d.ts +4 -0
  43. package/es/rce/plugins/instructure_rce_external_tools/components/util/ToolLaunchIframe.js +4 -0
  44. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.d.ts +11 -2
  45. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.js +92 -10
  46. package/es/rce/plugins/instructure_record/AudioOptionsTray/index.d.ts +13 -1
  47. package/es/rce/plugins/instructure_record/AudioOptionsTray/index.js +216 -24
  48. package/es/rce/plugins/instructure_record/MediaPanel/index.js +16 -5
  49. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.d.ts +14 -13
  50. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.js +110 -39
  51. package/es/rce/plugins/instructure_record/VideoOptionsTray/index.d.ts +11 -1
  52. package/es/rce/plugins/instructure_record/VideoOptionsTray/index.js +242 -67
  53. package/es/rce/plugins/instructure_record/clickCallback.js +19 -4
  54. package/es/rce/plugins/instructure_record/mediaTranslations.js +1 -1
  55. package/es/rce/plugins/instructure_record/playerLayoutOptions.d.ts +25 -0
  56. package/es/rce/plugins/instructure_record/playerLayoutOptions.js +91 -0
  57. package/es/rce/plugins/instructure_record/plugin.js +2 -5
  58. package/es/rce/plugins/instructure_record/utils.d.ts +3 -0
  59. package/es/rce/plugins/instructure_record/utils.js +31 -0
  60. package/es/rce/plugins/instructure_studio_media_options/plugin.js +82 -26
  61. package/es/rce/plugins/shared/ContentSelection.d.ts +6 -1
  62. package/es/rce/plugins/shared/ContentSelection.js +15 -6
  63. package/es/rce/plugins/shared/DimensionsInput/DimensionInput.js +1 -2
  64. package/es/rce/plugins/shared/DimensionsInput/index.js +11 -12
  65. package/es/rce/plugins/shared/DimensionsInput/useDimensionsState.d.ts +1 -1
  66. package/es/rce/plugins/shared/DimensionsInput/useDimensionsState.js +4 -3
  67. package/es/rce/plugins/shared/StudioLtiSupportUtils.d.ts +27 -6
  68. package/es/rce/plugins/shared/StudioLtiSupportUtils.js +82 -13
  69. package/es/rce/plugins/shared/Upload/UploadFile.js +1 -8
  70. package/es/rce/style.d.ts +2 -1
  71. package/es/rce/style.js +4 -2
  72. package/es/rcs/api.d.ts +5 -10
  73. package/es/rcs/api.js +15 -21
  74. package/es/rcs/fake.d.ts +1 -7
  75. package/es/rcs/fake.js +1 -47
  76. package/es/sidebar/actions/media.d.ts +19 -6
  77. package/es/sidebar/actions/media.js +17 -4
  78. package/es/sidebar/actions/upload.d.ts +3 -3
  79. package/es/sidebar/actions/upload.js +9 -9
  80. package/es/sidebar/containers/Sidebar.js +0 -2
  81. package/es/sidebar/containers/sidebarHandlers.d.ts +2 -4
  82. package/es/sidebar/containers/sidebarHandlers.js +2 -5
  83. package/es/sidebar/reducers/index.d.ts +0 -1
  84. package/es/sidebar/reducers/index.js +0 -2
  85. package/es/sidebar/store/initialState.d.ts +0 -1
  86. package/es/sidebar/store/initialState.js +0 -5
  87. package/es/translations/locales/ar.js +65 -80
  88. package/es/translations/locales/ca.js +65 -80
  89. package/es/translations/locales/cy.js +65 -80
  90. package/es/translations/locales/da-x-k12.js +65 -80
  91. package/es/translations/locales/da.js +65 -80
  92. package/es/translations/locales/de.js +65 -80
  93. package/es/translations/locales/el.js +0 -9
  94. package/es/translations/locales/en-AU-x-unimelb.js +65 -80
  95. package/es/translations/locales/en-GB-x-ukhe.js +65 -80
  96. package/es/translations/locales/en.js +61 -79
  97. package/es/translations/locales/en_AU.js +65 -80
  98. package/es/translations/locales/en_CA.js +65 -80
  99. package/es/translations/locales/en_CY.js +65 -80
  100. package/es/translations/locales/en_GB.js +65 -80
  101. package/es/translations/locales/es.js +65 -80
  102. package/es/translations/locales/es_ES.js +65 -80
  103. package/es/translations/locales/fa_IR.js +0 -9
  104. package/es/translations/locales/fi.js +65 -80
  105. package/es/translations/locales/fr.js +65 -80
  106. package/es/translations/locales/fr_CA.js +65 -80
  107. package/es/translations/locales/ga.js +65 -80
  108. package/es/translations/locales/he.js +0 -9
  109. package/es/translations/locales/hi.js +65 -80
  110. package/es/translations/locales/ht.js +65 -80
  111. package/es/translations/locales/hu.js +0 -36
  112. package/es/translations/locales/hy.js +0 -9
  113. package/es/translations/locales/id.js +65 -80
  114. package/es/translations/locales/is.js +65 -80
  115. package/es/translations/locales/it.js +65 -80
  116. package/es/translations/locales/ja.js +65 -80
  117. package/es/translations/locales/ko.js +2455 -133
  118. package/es/translations/locales/mi.js +65 -80
  119. package/es/translations/locales/ms.js +65 -80
  120. package/es/translations/locales/nb-x-k12.js +65 -80
  121. package/es/translations/locales/nb.js +65 -80
  122. package/es/translations/locales/nl.js +66 -81
  123. package/es/translations/locales/nn.js +0 -36
  124. package/es/translations/locales/pl.js +65 -80
  125. package/es/translations/locales/pt.js +65 -80
  126. package/es/translations/locales/pt_BR.js +65 -80
  127. package/es/translations/locales/ru.js +65 -80
  128. package/es/translations/locales/sl.js +65 -80
  129. package/es/translations/locales/sv-x-k12.js +65 -80
  130. package/es/translations/locales/sv.js +65 -80
  131. package/es/translations/locales/th.js +65 -80
  132. package/es/translations/locales/tr.js +1962 -18
  133. package/es/translations/locales/uk_UA.js +0 -9
  134. package/es/translations/locales/vi.js +65 -80
  135. package/es/translations/locales/zh-Hans.js +65 -80
  136. package/es/translations/locales/zh-Hant.js +65 -80
  137. package/es/translations/locales/zh.js +65 -80
  138. package/es/translations/locales/zh_HK.js +65 -80
  139. package/eslint.config.js +16 -147
  140. package/jest/jest-setup.js +1 -0
  141. package/jest.config.js +2 -0
  142. package/oxlint.json +84 -0
  143. package/package.json +87 -62
  144. package/tsconfig.json +3 -2
  145. package/es/rce/plugins/shared/ai_tools/AIResponseModal.d.ts +0 -10
  146. package/es/rce/plugins/shared/ai_tools/AIResponseModal.js +0 -67
  147. package/es/rce/plugins/shared/ai_tools/AIToolsTray.d.ts +0 -18
  148. package/es/rce/plugins/shared/ai_tools/AIToolsTray.js +0 -489
  149. package/es/rce/plugins/shared/ai_tools/aiicons.d.ts +0 -7
  150. package/es/rce/plugins/shared/ai_tools/aiicons.js +0 -60
  151. package/es/rce/plugins/shared/ai_tools/index.d.ts +0 -3
  152. package/es/sidebar/actions/flickr.d.ts +0 -20
  153. package/es/sidebar/actions/flickr.js +0 -60
  154. package/es/sidebar/reducers/flickr.d.ts +0 -1
  155. package/es/sidebar/reducers/flickr.js +0 -49
@@ -15,32 +15,37 @@
15
15
  * You should have received a copy of the GNU Affero General Public License along
16
16
  * with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
- import React, { useState, useEffect } from 'react';
19
- import { arrayOf, bool, func, number, shape, string } from 'prop-types';
20
- import { Button, CloseButton, IconButton } from '@instructure/ui-buttons';
18
+
19
+ import { ClosedCaptionPanel, ClosedCaptionPanelV2, CONSTANTS, trackPendoEvent } from '@instructure/canvas-media';
20
+ import { Button, CloseButton } from '@instructure/ui-buttons';
21
+ import { Checkbox, CheckboxGroup } from '@instructure/ui-checkbox';
22
+ import { Flex } from '@instructure/ui-flex';
23
+ import { FormFieldGroup } from '@instructure/ui-form-field';
21
24
  import { Heading } from '@instructure/ui-heading';
25
+ import { IconExternalLinkLine } from '@instructure/ui-icons';
26
+ import { Link } from '@instructure/ui-link';
22
27
  import { RadioInput, RadioInputGroup } from '@instructure/ui-radio-input';
23
28
  import { SimpleSelect } from '@instructure/ui-simple-select';
24
- import { TextArea } from '@instructure/ui-text-area';
25
- import { Text } from '@instructure/ui-text';
26
- import { IconQuestionLine } from '@instructure/ui-icons';
27
- import { Flex } from '@instructure/ui-flex';
28
- import { FormFieldGroup } from '@instructure/ui-form-field';
29
- import { View } from '@instructure/ui-view';
30
29
  import { Spinner } from '@instructure/ui-spinner';
30
+ import { Text } from '@instructure/ui-text';
31
+ import { TextInput } from '@instructure/ui-text-input';
31
32
  import { Tooltip } from '@instructure/ui-tooltip';
32
33
  import { Tray } from '@instructure/ui-tray';
33
- import { StoreProvider } from '../../shared/StoreContext';
34
- import { ClosedCaptionPanel } from '@instructure/canvas-media';
35
- import { CUSTOM, MIN_WIDTH_VIDEO, MIN_PERCENTAGE, videoSizes, studioPlayerSizes, labelForImageSize, scaleVideoSize, scaleToSize, MIN_WIDTH_STUDIO_PLAYER, MIN_HEIGHT_STUDIO_PLAYER } from '../../instructure_image/ImageEmbedOptions';
34
+ import { View } from '@instructure/ui-view';
35
+ import { arrayOf, bool, func, number, shape, string } from 'prop-types';
36
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
36
37
  import Bridge from '../../../../bridge';
37
- import RceApiSource from '../../../../rcs/api';
38
38
  import formatMessage from '../../../../format-message';
39
- import DimensionsInput, { useDimensionsState } from '../../shared/DimensionsInput';
40
- import { getTrayHeight } from '../../shared/trayUtils';
39
+ import RCEGlobals from '../../../../rce/RCEGlobals';
40
+ import RceApiSource, { originFromHost } from '../../../../rcs/api';
41
41
  import { instuiPopupMountNodeFn } from '../../../../util/fullscreenHelpers';
42
+ import { CUSTOM, labelForImageSize, MIN_HEIGHT_STUDIO_PLAYER, MIN_PERCENTAGE, MIN_WIDTH_STUDIO_PLAYER, scaleVideoSize, studioPlayerSizes } from '../../instructure_image/ImageEmbedOptions';
43
+ import DimensionsInput, { useDimensionsState } from '../../shared/DimensionsInput';
44
+ import { StoreProvider } from '../../shared/StoreContext';
42
45
  import { parsedStudioOptionsPropType } from '../../shared/StudioLtiSupportUtils';
43
- import RCEGlobals from '../../../../rce/RCEGlobals';
46
+ import { getTrayHeight } from '../../shared/trayUtils';
47
+ import { getPlayerLayoutSizes, labelForPlayerLayoutSize, playerLayoutDimensions, SMALL, scalePlayerLayoutForHeight, scalePlayerLayoutForWidth } from '../playerLayoutOptions';
48
+ import { mapStudioEmbedOptions, mapViewerRestrictions, readViewerRestrictions } from '../utils';
44
49
  const getLiveRegion = () => document.getElementById('flash_screenreader_holder');
45
50
  export default function VideoOptionsTray({
46
51
  videoOptions,
@@ -53,9 +58,13 @@ export default function VideoOptionsTray({
53
58
  onExited = null,
54
59
  id = 'video-options-tray',
55
60
  studioOptions = null,
56
- forBlockEditorUse = false
61
+ forBlockEditorUse = false,
62
+ onStudioEmbedOptionChanged = () => {},
63
+ onCaptionsModified = null,
64
+ isLoading = false
57
65
  }) {
58
- const isConsolidatedMediaPlayer = RCEGlobals.getFeatures()?.consolidated_media_player;
66
+ const isEmbedImprovements = RCEGlobals.getFeatures()?.rce_studio_embed_improvements;
67
+ const isAsrCaptioningImprovements = RCEGlobals.getFeatures()?.rce_asr_captioning_improvements;
59
68
  const {
60
69
  naturalHeight,
61
70
  naturalWidth
@@ -64,25 +73,56 @@ export default function VideoOptionsTray({
64
73
  const currentWidth = videoOptions.appliedWidth || naturalWidth;
65
74
  const [titleText, setTitleText] = useState(videoOptions.titleText);
66
75
  const [displayAs, setDisplayAs] = useState('embed');
67
- const [videoSize, setVideoSize] = useState(videoOptions.videoSize);
76
+ const [videoSize, setVideoSize] = useState(() => {
77
+ if (isAsrCaptioningImprovements) {
78
+ const match = Object.entries(playerLayoutDimensions).find(([, dims]) => dims.width === videoOptions.appliedWidth);
79
+ if (match) return match[0];
80
+ }
81
+ return videoOptions.videoSize;
82
+ });
68
83
  const [videoHeight, setVideoHeight] = useState(currentHeight);
69
84
  const [videoWidth, setVideoWidth] = useState(currentWidth);
70
85
  const [subtitles, setSubtitles] = useState(videoOptions.tracks || []);
71
- const [minWidth] = useState(isConsolidatedMediaPlayer ? MIN_WIDTH_STUDIO_PLAYER : MIN_WIDTH_VIDEO);
72
- const [minHeight] = useState(isConsolidatedMediaPlayer ? MIN_HEIGHT_STUDIO_PLAYER : Math.round(videoHeight / videoWidth * MIN_WIDTH_VIDEO));
86
+ const {
87
+ width: minWidth,
88
+ height: minHeight
89
+ } = isAsrCaptioningImprovements ? playerLayoutDimensions[SMALL] : {
90
+ width: MIN_WIDTH_STUDIO_PLAYER,
91
+ height: MIN_HEIGHT_STUDIO_PLAYER
92
+ };
73
93
  const [minPercentage] = useState(MIN_PERCENTAGE);
74
94
  const [editLocked, setEditLocked] = useState(null);
75
95
  const [loading, setLoading] = useState(true);
96
+ const [viewerRestrictions, setViewerRestrictions] = useState(() => readViewerRestrictions(videoOptions.viewerRestrictions));
97
+ const [studioEmbedOptions, setStudioEmbedOptions] = useState(() => mapStudioEmbedOptions(studioOptions?.embedOptions));
98
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
99
+ const fetchedFromIframeRef = useRef(false);
100
+ const titleInputRef = useRef(null);
76
101
  const isStudio = !!studioOptions;
102
+ const applyDescribedBy = useCallback(playerLayoutInput => {
103
+ if (isAsrCaptioningImprovements && !isStudio && playerLayoutInput) {
104
+ const helperId = `${id}-size-helper-text`;
105
+ const existing = playerLayoutInput.getAttribute('aria-describedby') || '';
106
+ const ids = existing.split(' ').filter(Boolean);
107
+ if (!ids.includes(helperId)) {
108
+ playerLayoutInput.setAttribute('aria-describedby', [...ids, helperId].join(' '));
109
+ }
110
+ }
111
+ }, [isAsrCaptioningImprovements, isStudio, id]);
77
112
  const showDisplayOptions = (!isStudio || studioOptions.convertibleToLink) && !forBlockEditorUse;
78
113
  const showSizeControls = (!isStudio || studioOptions.resizable) && !forBlockEditorUse;
79
114
  const dimensionsState = useDimensionsState(videoOptions, {
80
115
  minHeight,
81
116
  minWidth,
82
117
  minPercentage
83
- });
118
+ }, isAsrCaptioningImprovements ? {
119
+ scaleFns: {
120
+ width: scalePlayerLayoutForWidth,
121
+ height: scalePlayerLayoutForHeight
122
+ }
123
+ } : {});
84
124
  const api = new RceApiSource(trayProps);
85
- const videoSizeOptions = isConsolidatedMediaPlayer ? studioPlayerSizes : videoSizes;
125
+ const videoSizeOptions = isAsrCaptioningImprovements ? getPlayerLayoutSizes() : studioPlayerSizes;
86
126
  useEffect(() => {
87
127
  if (videoOptions.attachmentId) {
88
128
  api.getFile(videoOptions.attachmentId, {
@@ -96,8 +136,20 @@ export default function VideoOptionsTray({
96
136
  }
97
137
  }, [videoOptions.attachmentId]);
98
138
  useEffect(() => {
99
- if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles);
100
- }, []);
139
+ if (!isLoading && subtitles.length === 0 && !fetchedFromIframeRef.current) {
140
+ // only request subtitle data after mount
141
+ fetchedFromIframeRef.current = true;
142
+ requestSubtitlesFromIframe(setSubtitles);
143
+ }
144
+ }, [isLoading, subtitles.length, requestSubtitlesFromIframe]);
145
+ useEffect(() => {
146
+ if (open && isAsrCaptioningImprovements) {
147
+ trackPendoEvent('canvas_media_options_opened', {
148
+ entry_point: 'quick_menu',
149
+ media_kind: 'video'
150
+ });
151
+ }
152
+ }, [open, isAsrCaptioningImprovements]);
101
153
  function handleTitleTextChange(event) {
102
154
  setTitleText(event.target.value);
103
155
  }
@@ -110,11 +162,18 @@ export default function VideoOptionsTray({
110
162
  if (selectedOption.value === CUSTOM) {
111
163
  setVideoHeight(currentHeight);
112
164
  setVideoWidth(currentWidth);
165
+ } else if (isAsrCaptioningImprovements) {
166
+ const {
167
+ width,
168
+ height
169
+ } = playerLayoutDimensions[selectedOption.value];
170
+ setVideoHeight(height);
171
+ setVideoWidth(width);
113
172
  } else {
114
173
  const {
115
174
  height,
116
175
  width
117
- } = isConsolidatedMediaPlayer ? scaleVideoSize(selectedOption.value, naturalWidth, naturalHeight) : scaleToSize(selectedOption.value, naturalWidth, naturalHeight);
176
+ } = scaleVideoSize(selectedOption.value, naturalWidth, naturalHeight);
118
177
  setVideoHeight(height);
119
178
  setVideoWidth(width);
120
179
  }
@@ -122,14 +181,33 @@ export default function VideoOptionsTray({
122
181
  function handleUpdateSubtitles(new_subtitles) {
123
182
  setSubtitles(new_subtitles);
124
183
  }
184
+ const handleEmbedOptionChange = useCallback(options => {
185
+ const mappedOptions = options.reduce((a, c) => {
186
+ a[c] = options.includes(c);
187
+ return a;
188
+ }, {});
189
+ setStudioEmbedOptions(options);
190
+ onStudioEmbedOptionChanged(mappedOptions);
191
+ }, [onStudioEmbedOptionChanged]);
125
192
  function handleSave(event, updateMediaObject) {
126
193
  event.preventDefault();
194
+ if (titleText.trim() === '') {
195
+ if (titleInputRef.current) {
196
+ titleInputRef.current.focus();
197
+ }
198
+ return;
199
+ }
127
200
  let appliedHeight = videoHeight;
128
201
  let appliedWidth = videoWidth;
129
202
  if (videoSize === CUSTOM) {
130
203
  appliedHeight = dimensionsState.height;
131
204
  appliedWidth = dimensionsState.width;
132
205
  }
206
+ if (isAsrCaptioningImprovements) {
207
+ trackPendoEvent('canvas_player_layout_selected', {
208
+ layout_type: videoSize.replace('-', '_')
209
+ });
210
+ }
133
211
  onSave({
134
212
  media_object_id: videoOptions.id,
135
213
  attachment_id: videoOptions.attachmentId,
@@ -139,31 +217,15 @@ export default function VideoOptionsTray({
139
217
  displayAs,
140
218
  subtitles,
141
219
  updateMediaObject,
142
- editLocked
220
+ editLocked,
221
+ viewerRestrictions: mapViewerRestrictions(viewerRestrictions)
143
222
  });
144
223
  }
145
- const tooltipText = formatMessage('Used by screen readers to describe the video');
146
- const textAreaLabel = /*#__PURE__*/React.createElement(Flex, {
147
- alignItems: "center"
148
- }, /*#__PURE__*/React.createElement(Flex.Item, null, formatMessage('Title')), /*#__PURE__*/React.createElement(Flex.Item, {
149
- margin: "0 0 0 xx-small"
150
- }, /*#__PURE__*/React.createElement(Tooltip, {
151
- on: ['hover', 'focus'],
152
- placement: "top",
153
- renderTip: /*#__PURE__*/React.createElement(View, {
154
- display: "block",
155
- id: "alt-text-label-tooltip",
156
- maxWidth: "14rem"
157
- }, tooltipText)
158
- }, /*#__PURE__*/React.createElement(IconButton, {
159
- renderIcon: IconQuestionLine,
160
- size: "small",
161
- screenReaderLabel: tooltipText,
162
- withBackground: false,
163
- withBorder: false
164
- }))));
224
+ const handleDirtyCheck = isDirty => {
225
+ setHasUnsavedChanges(isDirty);
226
+ };
165
227
  const messagesForSize = [];
166
- if (videoSize !== CUSTOM) {
228
+ if (videoSize !== CUSTOM && !isAsrCaptioningImprovements) {
167
229
  messagesForSize.push({
168
230
  text: formatMessage('{width} x {height}px', {
169
231
  height: videoHeight,
@@ -172,7 +234,7 @@ export default function VideoOptionsTray({
172
234
  type: 'hint'
173
235
  });
174
236
  }
175
- const saveDisabled = displayAs === 'embed' && (titleText === '' || videoSize === CUSTOM && !dimensionsState.isValid);
237
+ const saveDisabled = displayAs === 'embed' && videoSize === CUSTOM && !dimensionsState.isValid;
176
238
  return /*#__PURE__*/React.createElement(StoreProvider, trayProps, contentProps => /*#__PURE__*/React.createElement(Tray, {
177
239
  key: "video-options-tray",
178
240
  "data-mce-component": true,
@@ -204,7 +266,7 @@ export default function VideoOptionsTray({
204
266
  color: "primary",
205
267
  onClick: onRequestClose,
206
268
  screenReaderLabel: formatMessage('Close')
207
- })))), loading && videoOptions.attachmentId ? /*#__PURE__*/React.createElement(Flex.Item, {
269
+ })))), loading && videoOptions.attachmentId || isLoading ? /*#__PURE__*/React.createElement(Flex.Item, {
208
270
  textAlign: "center",
209
271
  margin: "xx-large",
210
272
  padding: "xx-large"
@@ -233,15 +295,18 @@ export default function VideoOptionsTray({
233
295
  weight: "bold"
234
296
  }, formatMessage('Media Title'))), /*#__PURE__*/React.createElement(Flex.Item, {
235
297
  padding: "small none none small"
236
- }, titleText)) : /*#__PURE__*/React.createElement(TextArea, {
237
- "aria-describedby": "alt-text-label-tooltip",
238
- disabled: displayAs === 'link',
239
- height: "4rem",
240
- label: textAreaLabel,
298
+ }, titleText)) : /*#__PURE__*/React.createElement(TextInput, {
299
+ interaction: displayAs === 'link' ? 'disabled' : 'enabled',
300
+ renderLabel: formatMessage('Title'),
241
301
  onChange: handleTitleTextChange,
242
- placeholder: formatMessage('(Describe the video)'),
243
- resize: "vertical",
244
- value: titleText
302
+ placeholder: formatMessage('Enter a media title'),
303
+ value: titleText,
304
+ inputRef: el => titleInputRef.current = el,
305
+ messages: titleText?.trim() === '' ? [{
306
+ text: formatMessage("Title can't be blank"),
307
+ type: 'newError'
308
+ }] : [],
309
+ isRequired: true
245
310
  })), showDisplayOptions && /*#__PURE__*/React.createElement(Flex.Item, {
246
311
  margin: "small none none none",
247
312
  padding: "small"
@@ -262,10 +327,11 @@ export default function VideoOptionsTray({
262
327
  as: "div",
263
328
  padding: "small small xx-small small"
264
329
  }, /*#__PURE__*/React.createElement(SimpleSelect, {
330
+ inputRef: applyDescribedBy,
265
331
  id: `${id}-size`,
266
332
  mountNode: instuiPopupMountNodeFn,
267
333
  disabled: displayAs !== 'embed',
268
- renderLabel: formatMessage('Size'),
334
+ renderLabel: isAsrCaptioningImprovements ? formatMessage('Player layout') : formatMessage('Size'),
269
335
  messages: messagesForSize,
270
336
  assistiveText: formatMessage('Use arrow keys to navigate options.'),
271
337
  onChange: handleVideoSizeChange,
@@ -274,7 +340,13 @@ export default function VideoOptionsTray({
274
340
  id: `${id}-size-${size}`,
275
341
  key: size,
276
342
  value: size
277
- }, labelForImageSize(size))))), videoSize === CUSTOM && /*#__PURE__*/React.createElement(View, {
343
+ }, isAsrCaptioningImprovements ? labelForPlayerLayoutSize(size) : labelForImageSize(size)))), isAsrCaptioningImprovements && !isStudio && /*#__PURE__*/React.createElement(View, {
344
+ as: "div",
345
+ id: `${id}-size-helper-text`,
346
+ margin: "xx-small none none none"
347
+ }, /*#__PURE__*/React.createElement(Text, {
348
+ size: "small"
349
+ }, formatMessage('Transcript panel is available at widths above 720px.')))), videoSize === CUSTOM && /*#__PURE__*/React.createElement(View, {
278
350
  as: "div",
279
351
  padding: "xx-small small"
280
352
  }, /*#__PURE__*/React.createElement(DimensionsInput, {
@@ -284,11 +356,33 @@ export default function VideoOptionsTray({
284
356
  minWidth: minWidth,
285
357
  minPercentage: minPercentage,
286
358
  hidePercentage: true
359
+ }))), isAsrCaptioningImprovements && !isStudio && /*#__PURE__*/React.createElement(Flex.Item, {
360
+ padding: "small"
361
+ }, /*#__PURE__*/React.createElement(CheckboxGroup, {
362
+ name: "viewer-restrictions",
363
+ onChange: setViewerRestrictions,
364
+ defaultValue: viewerRestrictions,
365
+ description: /*#__PURE__*/React.createElement(Heading, {
366
+ level: "h4",
367
+ as: "h3"
368
+ }, formatMessage('Viewer Restrictions'))
369
+ }, /*#__PURE__*/React.createElement(Checkbox, {
370
+ variant: "toggle",
371
+ label: formatMessage('Show Rolling Transcript'),
372
+ value: "show_rolling_transcript",
373
+ messages: [{
374
+ text: formatMessage('Transcript panel is available at widths above 720px.'),
375
+ type: 'screenreader-only'
376
+ }]
287
377
  }))), !isStudio && !editLocked && /*#__PURE__*/React.createElement(Flex.Item, {
288
378
  padding: "small"
289
379
  }, /*#__PURE__*/React.createElement(FormFieldGroup, {
290
- description: formatMessage('Closed Captions/Subtitles')
291
- }, /*#__PURE__*/React.createElement(ClosedCaptionPanel, {
380
+ description: /*#__PURE__*/React.createElement(Heading, {
381
+ level: "h4",
382
+ as: "h3"
383
+ }, isAsrCaptioningImprovements ? formatMessage('Caption Manager') : formatMessage('Closed Captions/Subtitles'))
384
+ }, !isAsrCaptioningImprovements && /*#__PURE__*/React.createElement(ClosedCaptionPanel, {
385
+ key: subtitles.reduce((acc, track) => acc + track.locale, ''),
292
386
  subtitles: subtitles.map(st => ({
293
387
  locale: st.locale,
294
388
  inherited: st.inherited,
@@ -301,16 +395,91 @@ export default function VideoOptionsTray({
301
395
  updateSubtitles: handleUpdateSubtitles,
302
396
  liveRegion: getLiveRegion,
303
397
  mountNode: instuiPopupMountNodeFn
304
- }))))), /*#__PURE__*/React.createElement(Flex.Item, {
398
+ }), isAsrCaptioningImprovements && /*#__PURE__*/React.createElement(ClosedCaptionPanelV2, {
399
+ subtitles: subtitles.map(st => ({
400
+ ...st,
401
+ file: {
402
+ name: st.language || st.locale
403
+ },
404
+ asr: Boolean(st.asr)
405
+ })),
406
+ uploadMediaTranslations: Bridge.uploadMediaTranslations,
407
+ userLocale: Bridge.userLocale,
408
+ onUpdateSubtitles: handleUpdateSubtitles,
409
+ liveRegion: getLiveRegion,
410
+ mountNode: instuiPopupMountNodeFn,
411
+ uploadConfig: {
412
+ mediaObjectId: videoOptions.id,
413
+ attachmentId: videoOptions.attachmentId,
414
+ origin: originFromHost(api.host),
415
+ headers: api.jwt ? {
416
+ Authorization: `Bearer ${api.jwt}`
417
+ } : undefined,
418
+ maxBytes: CONSTANTS.CC_FILE_MAX_BYTES
419
+ },
420
+ onCaptionUploaded: subtitle => {
421
+ // Update local state so "Done" button knows about it
422
+ setSubtitles(prev => [...prev.filter(s => s.locale !== subtitle.locale), subtitle]);
423
+ onCaptionsModified?.();
424
+ },
425
+ onCaptionDeleted: locale => {
426
+ setSubtitles(prev => prev.filter(s => s.locale !== locale));
427
+ onCaptionsModified?.();
428
+ },
429
+ onDirtyStateChanged: handleDirtyCheck
430
+ }))), isStudio && isEmbedImprovements && studioOptions.isImprovedEmbed ? /*#__PURE__*/React.createElement(Flex.Item, {
431
+ padding: "small"
432
+ }, /*#__PURE__*/React.createElement(CheckboxGroup, {
433
+ name: "studio-embed-options",
434
+ onChange: handleEmbedOptionChange,
435
+ value: studioEmbedOptions,
436
+ description: /*#__PURE__*/React.createElement(Heading, {
437
+ level: "h4",
438
+ as: "h3"
439
+ }, formatMessage('Viewer Restrictions'))
440
+ }, /*#__PURE__*/React.createElement(Text, {
441
+ variant: "contentSmall"
442
+ }, formatMessage('Changes will apply after you save this page.')), /*#__PURE__*/React.createElement(Checkbox, {
443
+ label: formatMessage('Lock speed at 1x'),
444
+ value: "lockSpeed",
445
+ variant: "toggle"
446
+ }), !studioOptions?.embedOptions?.isExternal ? /*#__PURE__*/React.createElement(Checkbox, {
447
+ label: formatMessage('Allow media download'),
448
+ value: "enableMediaDownload",
449
+ variant: "toggle"
450
+ }) : null, /*#__PURE__*/React.createElement(Checkbox, {
451
+ label: formatMessage('Allow transcript download'),
452
+ value: "enableTranscriptDownload",
453
+ variant: "toggle"
454
+ }), /*#__PURE__*/React.createElement(Checkbox, {
455
+ label: formatMessage('Show rolling transcript'),
456
+ value: "showRollingTranscript",
457
+ variant: "toggle"
458
+ }))) : null, !isStudio && isAsrCaptioningImprovements ? /*#__PURE__*/React.createElement(Flex.Item, {
459
+ padding: "small"
460
+ }, /*#__PURE__*/React.createElement(Link, {
461
+ id: "tray-transcript-help-link",
462
+ variant: "standalone",
463
+ renderIcon: /*#__PURE__*/React.createElement(IconExternalLinkLine, null),
464
+ href: "https://productmarketing.instructuremedia.com/embed/32388c5a-580c-40f0-85a2-6b4042ddcccb",
465
+ target: "_blank",
466
+ rel: "noopener noreferrer"
467
+ }, formatMessage('How to request and edit captions?'))) : null)), /*#__PURE__*/React.createElement(Flex.Item, {
305
468
  background: "secondary",
306
469
  borderWidth: "small none none none",
307
470
  padding: "small medium",
308
471
  textAlign: "end"
472
+ }, /*#__PURE__*/React.createElement(Tooltip, {
473
+ renderTip: formatMessage('Unsaved changes will be lost.'),
474
+ placement: "top",
475
+ on: ['hover', 'focus'],
476
+ preventTooltip: !hasUnsavedChanges,
477
+ mountNode: instuiPopupMountNodeFn
309
478
  }, /*#__PURE__*/React.createElement(Button, {
310
- disabled: saveDisabled,
479
+ interaction: saveDisabled ? 'disabled' : 'enabled',
311
480
  onClick: event => handleSave(event, contentProps.updateMediaObject),
312
481
  color: "primary"
313
- }, formatMessage('Done'))))))));
482
+ }, formatMessage('Done')))))))));
314
483
  }
315
484
  VideoOptionsTray.propTypes = {
316
485
  videoOptions: shape({
@@ -322,7 +491,10 @@ VideoOptionsTray.propTypes = {
322
491
  tracks: arrayOf(shape({
323
492
  locale: string.isRequired,
324
493
  inherited: bool
325
- }))
494
+ })),
495
+ viewerRestrictions: shape({
496
+ show_rolling_transcript: bool
497
+ })
326
498
  }).isRequired,
327
499
  onEntered: func,
328
500
  onExited: func,
@@ -335,5 +507,8 @@ VideoOptionsTray.propTypes = {
335
507
  }),
336
508
  id: string,
337
509
  studioOptions: parsedStudioOptionsPropType,
338
- requestSubtitlesFromIframe: func
510
+ requestSubtitlesFromIframe: func,
511
+ onStudioEmbedOptionChanged: func,
512
+ onCaptionsModified: func,
513
+ isLoading: bool
339
514
  };
@@ -16,14 +16,15 @@
16
16
  * with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
18
 
19
+ import { trackPendoEvent } from '@instructure/canvas-media';
19
20
  import React from 'react';
20
21
  import ReactDOM from 'react-dom';
21
22
  import Bridge from '../../../bridge';
22
- import { StoreProvider } from '../shared/StoreContext';
23
23
  import formatMessage from '../../../format-message';
24
24
  import { headerFor, originFromHost } from '../../../rcs/api';
25
25
  import { instuiPopupMountNodeFn } from '../../../util/fullscreenHelpers';
26
26
  import RCEGlobals from '../../RCEGlobals';
27
+ import { StoreProvider } from '../shared/StoreContext';
27
28
  export const handleUpload = (error, uploadData, onUploadComplete, uploadBookmark) => {
28
29
  let err_msg = error && Bridge.uploadMediaTranslations.UploadMediaStrings.UPLOADING_ERROR;
29
30
  if (error?.name === 'FileSizeError') {
@@ -84,15 +85,29 @@ export default function (ed, document) {
84
85
  open: true,
85
86
  liveRegion: () => document.getElementById('flash_screenreader_holder'),
86
87
  onStartUpload: fileProps => handleStartUpload(fileProps),
87
- onUploadComplete: (err, data) => handleUpload(err, data, contentProps.mediaUploadComplete, uploadBookmark),
88
+ onUploadComplete: (err, data) => {
89
+ if (!err && data) {
90
+ if (RCEGlobals.getFeatures()?.rce_asr_captioning_improvements) {
91
+ var _data$uploadedFile$ty;
92
+ const fileType = (_data$uploadedFile$ty = data?.uploadedFile?.type) !== null && _data$uploadedFile$ty !== void 0 ? _data$uploadedFile$ty : '';
93
+ trackPendoEvent('canvas_native_media_embedded', {
94
+ insertion_method: 'insert_menu',
95
+ media_id: data?.mediaObject?.media_object?.media_id,
96
+ media_kind: fileType.startsWith('audio/') ? 'audio' : fileType.startsWith('video/') ? 'video' : undefined,
97
+ resourceType: ed.settings.canvas_rce_user_context.type,
98
+ resourceId: ed.settings.canvas_rce_user_context.id
99
+ });
100
+ }
101
+ }
102
+ handleUpload(err, data, contentProps.mediaUploadComplete, uploadBookmark);
103
+ },
88
104
  onDismiss: handleDismiss,
89
105
  tabs: {
90
106
  record: true,
91
107
  upload: true
92
108
  },
93
109
  uploadMediaTranslations: Bridge.uploadMediaTranslations,
94
- media_links_use_attachment_id: RCEGlobals.getFeatures().media_links_use_attachment_id,
95
- useStudioPlayer: RCEGlobals.getFeatures()?.consolidated_media_player
110
+ media_links_use_attachment_id: RCEGlobals.getFeatures().media_links_use_attachment_id
96
111
  })), container);
97
112
  });
98
113
  }
@@ -43,7 +43,7 @@ const uploadMediaTranslations = {
43
43
  SUPPORTED_FILE_TYPES: formatMessage('Supported file types: SRT or WebVTT'),
44
44
  NO_FILE_CHOSEN: formatMessage('No file chosen'),
45
45
  REMOVE_FILE: 'Remove {lang} closed captions',
46
- ADD_NEW_CAPTION_OR_SUBTITLE: formatMessage('Add another'),
46
+ ADD_NEW_CAPTION_OR_SUBTITLE: formatMessage('Add another closed caption or subtitle'),
47
47
  ADDED_CAPTION: 'Captions for {lang} added',
48
48
  DELETED_CAPTION: 'Deleted captions for {lang}',
49
49
  PROGRESS_LABEL: formatMessage('Uploading'),
@@ -0,0 +1,25 @@
1
+ export declare const SMALL = "small";
2
+ export declare const MEDIUM = "medium";
3
+ export declare const LARGE = "large";
4
+ export declare const EXTRA_LARGE = "extra-large";
5
+ export declare const CUSTOM = "custom";
6
+ export declare const PLAYER_CONTROLS_HEIGHT = 48;
7
+ export declare const TRANSCRIPT_SIDEBAR_WIDTH = 300;
8
+ export declare const TRANSCRIPT_SIDEBAR_THRESHOLD = 720;
9
+ export declare const playerLayoutDimensions: Record<string, {
10
+ width: number;
11
+ height: number;
12
+ }>;
13
+ declare const playerLayoutSizes: readonly ["small", "medium", "large", "extra-large", "custom"];
14
+ export type PlayerLayoutSize = (typeof playerLayoutSizes)[number];
15
+ export declare function getPlayerLayoutSizes(): readonly PlayerLayoutSize[];
16
+ export declare function labelForPlayerLayoutSize(size: string): string;
17
+ export declare function scalePlayerLayoutForWidth(_naturalWidth: number, _naturalHeight: number, targetWidth: number | null): {
18
+ width: number | null;
19
+ height: number | null;
20
+ };
21
+ export declare function scalePlayerLayoutForHeight(_naturalWidth: number, _naturalHeight: number, targetHeight: number | null): {
22
+ width: number | null;
23
+ height: number | null;
24
+ };
25
+ export {};
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Copyright (C) 2026 - 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
+ export const SMALL = 'small';
21
+ export const MEDIUM = 'medium';
22
+ export const LARGE = 'large';
23
+ export const EXTRA_LARGE = 'extra-large';
24
+ export const CUSTOM = 'custom';
25
+ export const PLAYER_CONTROLS_HEIGHT = 48;
26
+ export const TRANSCRIPT_SIDEBAR_WIDTH = 300;
27
+ export const TRANSCRIPT_SIDEBAR_THRESHOLD = 720;
28
+ export const playerLayoutDimensions = {
29
+ [SMALL]: {
30
+ width: 400,
31
+ height: 273
32
+ },
33
+ [MEDIUM]: {
34
+ width: 480,
35
+ height: 318
36
+ },
37
+ [LARGE]: {
38
+ width: 700,
39
+ height: 442
40
+ },
41
+ [EXTRA_LARGE]: {
42
+ width: 850,
43
+ height: 357
44
+ }
45
+ };
46
+ const playerLayoutSizes = [SMALL, MEDIUM, LARGE, EXTRA_LARGE, CUSTOM];
47
+ export function getPlayerLayoutSizes() {
48
+ return playerLayoutSizes;
49
+ }
50
+ export function labelForPlayerLayoutSize(size) {
51
+ const dims = playerLayoutDimensions[size];
52
+ switch (size) {
53
+ case SMALL:
54
+ return formatMessage('Small ({width} x {height}px)', dims);
55
+ case MEDIUM:
56
+ return formatMessage('Medium ({width} x {height}px)', dims);
57
+ case LARGE:
58
+ return formatMessage('Large ({width} x {height}px)', dims);
59
+ case EXTRA_LARGE:
60
+ return formatMessage('Extra Large ({width} x {height}px)', dims);
61
+ default:
62
+ return formatMessage('Custom');
63
+ }
64
+ }
65
+
66
+ // Scale functions for the Custom player layout option.
67
+ // Signature matches scaleForWidth/scaleForHeight in DimensionUtils.js
68
+ // (naturalWidth/naturalHeight are unused here — the formula is fixed).
69
+ export function scalePlayerLayoutForWidth(_naturalWidth, _naturalHeight, targetWidth) {
70
+ if (targetWidth == null) return {
71
+ width: null,
72
+ height: null
73
+ };
74
+ const videoWidth = targetWidth > TRANSCRIPT_SIDEBAR_THRESHOLD ? targetWidth - TRANSCRIPT_SIDEBAR_WIDTH : targetWidth;
75
+ return {
76
+ width: targetWidth,
77
+ height: Math.round(videoWidth * (9 / 16) + PLAYER_CONTROLS_HEIGHT)
78
+ };
79
+ }
80
+ export function scalePlayerLayoutForHeight(_naturalWidth, _naturalHeight, targetHeight) {
81
+ if (targetHeight == null) return {
82
+ width: null,
83
+ height: null
84
+ };
85
+ const videoWidth = (targetHeight - PLAYER_CONTROLS_HEIGHT) * (16 / 9);
86
+ const totalWidth = videoWidth > TRANSCRIPT_SIDEBAR_THRESHOLD ? Math.round(videoWidth + TRANSCRIPT_SIDEBAR_WIDTH) : Math.round(videoWidth);
87
+ return {
88
+ width: totalWidth,
89
+ height: targetHeight
90
+ };
91
+ }
@@ -131,14 +131,12 @@ tinymce.PluginManager.add('instructure_record', function (ed) {
131
131
  * Register the Video "Options" button that will open the Video Options
132
132
  * tray.
133
133
  */
134
- const buttonAriaLabel = formatMessage('Show video options');
135
134
  ed.ui.registry.addButton('instructure-video-options', {
136
135
  onAction() {
137
136
  // show the tray
138
137
  videoTrayController.showTrayForEditor(ed);
139
138
  },
140
- text: formatMessage('Video Options'),
141
- tooltip: buttonAriaLabel
139
+ text: formatMessage('Video Options')
142
140
  });
143
141
  ed.ui.registry.addContextToolbar('instructure-video-toolbar', {
144
142
  items: 'instructure-video-options',
@@ -150,8 +148,7 @@ tinymce.PluginManager.add('instructure_record', function (ed) {
150
148
  onAction() {
151
149
  audioTrayController.showTrayForEditor(ed);
152
150
  },
153
- text: formatMessage('Audio Options'),
154
- tooltip: formatMessage('Show audio options')
151
+ text: formatMessage('Audio Options')
155
152
  });
156
153
  ed.ui.registry.addContextToolbar('instructure-audio-toolbar', {
157
154
  items: 'instructure-audio-options',