@instructure/canvas-rce 7.3.1 → 8.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 (155) hide show
  1. package/CHANGELOG.md +65 -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 +86 -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
@@ -4,14 +4,23 @@ export default class TrayController {
4
4
  _shouldOpen: boolean;
5
5
  _editor: any;
6
6
  _audioContainer: Element | null;
7
+ _captionsModified: boolean;
8
+ requestSubtitlesFromIframe(cb: any): void;
7
9
  get container(): HTMLElement;
8
10
  get isOpen(): boolean;
9
11
  showTrayForEditor(editor: any): void;
12
+ _isPlayerReady: boolean | undefined;
10
13
  hideTrayForEditor(editor: any): void;
14
+ _listenForPlayerIframeToLoad(currentMediaId: any): void;
15
+ _iframeLoadingListener: AbortController | undefined;
16
+ _reloadAudioPlayer(): void;
11
17
  _dismissTray(): void;
12
18
  _resetController(): Node;
19
+ _resizeContainer({ height, width }: {
20
+ height: any;
21
+ width: any;
22
+ }): void;
13
23
  _applyAudioOptions(audioOptions: any): any;
14
- requestSubtitlesFromIframe(cb: any): void;
15
24
  _subtitleListener: AbortController | undefined;
16
- _renderTray(trayProps: any): void;
25
+ _renderTray(): void;
17
26
  }
@@ -19,6 +19,9 @@
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
+ import { showFlashAlert } from '../../../../common/FlashAlert';
23
+ import formatMessage from '../../../../format-message';
24
+ import RCEGlobals from '../../../RCEGlobals';
22
25
  import { asAudioElement } from '../../shared/ContentSelection';
23
26
  import { findMediaPlayerIframe } from '../../shared/iframeUtils';
24
27
  import AudioOptionsTray from '.';
@@ -29,6 +32,8 @@ export default class TrayController {
29
32
  this._shouldOpen = false;
30
33
  this._editor = null;
31
34
  this._audioContainer = null;
35
+ this._captionsModified = false;
36
+ this.requestSubtitlesFromIframe = this.requestSubtitlesFromIframe.bind(this);
32
37
  }
33
38
  get container() {
34
39
  let _container = document.getElementById(CONTAINER_ID);
@@ -46,19 +51,57 @@ export default class TrayController {
46
51
  this._shouldOpen = true;
47
52
  this._editor = editor;
48
53
  this._audioContainer = findMediaPlayerIframe(editor.selection.getNode());
54
+ this._captionsModified = false;
55
+ this._isPlayerReady = false;
49
56
  if (bridge.focusedEditor) {
50
57
  // Dismiss any content trays that may already be open
51
58
  bridge.hideTrays();
52
59
  }
53
- const trayProps = bridge.trayProps.get(editor);
54
- this._renderTray(trayProps);
60
+ this._renderTray();
61
+ const audioOptions = asAudioElement(this._audioContainer);
62
+ // Clean broadcast listeners for any existing trays which are not shown (if not cleaned automatically)
63
+ this._iframeLoadingListener?.abort();
64
+ this._listenForPlayerIframeToLoad(audioOptions.id);
55
65
  }
56
66
  hideTrayForEditor(editor) {
57
67
  if (this._editor === editor) {
58
68
  this._dismissTray();
59
69
  }
60
70
  }
71
+ _listenForPlayerIframeToLoad(currentMediaId) {
72
+ if (!bridge.canvasOrigin) return;
73
+ this._iframeLoadingListener = new AbortController();
74
+
75
+ // Wait for player iframe to be loaded
76
+ window.addEventListener('message', event => {
77
+ // If tray was opened before player iframe was ready it will catch ready event.
78
+ // If not it will request it later and catch it here anyway.
79
+ if (event.data?.subject === 'media_player.iframe_ready' && event.data?.mediaId === currentMediaId) {
80
+ this._iframeLoadingListener.abort();
81
+ this._isPlayerReady = true;
82
+ this._renderTray();
83
+ }
84
+ }, {
85
+ signal: this._iframeLoadingListener.signal
86
+ });
87
+
88
+ // If tray was opened after player was loaded we need to request iframe_ready state
89
+ this._audioContainer?.contentWindow?.postMessage({
90
+ subject: 'media_player.get_ready_state'
91
+ }, bridge.canvasOrigin);
92
+ }
93
+ _reloadAudioPlayer() {
94
+ if (this._audioContainer?.contentWindow?.location) {
95
+ this._audioContainer.contentWindow.location.reload();
96
+ }
97
+ }
61
98
  _dismissTray() {
99
+ const isCaptionImprovements = RCEGlobals.getFeatures()?.rce_asr_captioning_improvements || false;
100
+
101
+ // Reload if captions were modified AND feature flag enabled
102
+ if (isCaptionImprovements && this._captionsModified && this._audioContainer) {
103
+ this._reloadAudioPlayer();
104
+ }
62
105
  if (this._audioContainer) {
63
106
  this._editor.selection.select(this._audioContainer);
64
107
  }
@@ -66,24 +109,50 @@ export default class TrayController {
66
109
  }
67
110
  _resetController() {
68
111
  this._shouldOpen = false;
69
- const trayProps = bridge.trayProps.get(this._editor);
70
- this._renderTray(trayProps);
112
+ this._renderTray();
71
113
  this._editor = null;
72
114
  this._audioContainer = null;
115
+ this._iframeLoadingListener?.abort();
73
116
  const elem = document.getElementById(CONTAINER_ID);
74
117
  return elem.parentNode.removeChild(elem);
75
118
  }
119
+ _resizeContainer({
120
+ height,
121
+ width
122
+ }) {
123
+ const styles = {
124
+ height: `${height}px`,
125
+ width: `${width}px`
126
+ };
127
+ this._editor.dom.setStyles(this._audioContainer.parentElement, styles);
128
+ this._editor.dom.setStyles(this._audioContainer, styles);
129
+
130
+ // tell tinymce so the context toolbar resets
131
+ this._editor.fire('ObjectResized', {
132
+ target: this._audioContainer,
133
+ width: width,
134
+ height: height
135
+ });
136
+ }
76
137
  _applyAudioOptions(audioOptions) {
138
+ this._resizeContainer({
139
+ width: audioOptions.appliedWidth,
140
+ height: audioOptions.appliedHeight
141
+ });
77
142
  const hasAttachmentId = audioOptions.attachment_id;
78
143
  if (!hasAttachmentId && (!audioOptions.media_object_id || audioOptions.media_object_id === 'undefined')) {
79
144
  return;
80
145
  }
81
146
  const container = this._audioContainer;
82
- return audioOptions.updateMediaObject({
147
+ const isCaptionImprovements = RCEGlobals.getFeatures()?.rce_asr_captioning_improvements || false;
148
+ const data = {
83
149
  media_object_id: audioOptions.media_object_id,
150
+ attachment_id: audioOptions.attachment_id,
84
151
  subtitles: audioOptions.subtitles,
85
- attachment_id: audioOptions.attachment_id
86
- }).then(() => container?.contentWindow.location.reload()).catch(ex => {
152
+ skipCaptionUpdate: isCaptionImprovements,
153
+ viewerRestrictions: audioOptions.viewerRestrictions
154
+ };
155
+ return audioOptions.updateMediaObject(data).then(() => container?.contentWindow.location.reload()).catch(ex => {
87
156
  console.error('Failed updating audio captions', ex);
88
157
  });
89
158
  }
@@ -101,9 +170,10 @@ export default class TrayController {
101
170
  subject: 'media_tracks_request'
102
171
  }, bridge.canvasOrigin);
103
172
  }
104
- _renderTray(trayProps) {
173
+ _renderTray() {
105
174
  const audioOptions = asAudioElement(this._audioContainer) || {};
106
175
  const element = /*#__PURE__*/React.createElement(AudioOptionsTray, {
176
+ key: audioOptions.id,
107
177
  audioOptions: audioOptions,
108
178
  onEntered: () => {
109
179
  this._isOpen = true;
@@ -112,15 +182,27 @@ export default class TrayController {
112
182
  bridge.focusActiveEditor(false);
113
183
  this._isOpen = false;
114
184
  this._subtitleListener?.abort();
185
+ this._iframeLoadingListener?.abort();
186
+ this._isPlayerReady = false;
115
187
  },
116
188
  onSave: options => {
117
189
  this._applyAudioOptions(options);
118
190
  this._dismissTray();
191
+ setTimeout(() => {
192
+ showFlashAlert({
193
+ message: formatMessage('Media options saved.'),
194
+ type: 'success'
195
+ });
196
+ }, 0);
119
197
  },
120
198
  onDismiss: () => this._dismissTray(),
199
+ onCaptionsModified: () => {
200
+ this._captionsModified = true;
201
+ },
121
202
  open: this._shouldOpen,
122
- trayProps: trayProps,
123
- requestSubtitlesFromIframe: cb => this.requestSubtitlesFromIframe(cb)
203
+ trayProps: bridge.trayProps.get(this._editor),
204
+ requestSubtitlesFromIframe: this.requestSubtitlesFromIframe,
205
+ isLoading: !this._isPlayerReady
124
206
  });
125
207
  ReactDOM.render(element, this.container);
126
208
  }
@@ -1,4 +1,4 @@
1
- declare function AudioOptionsTray({ open, onEntered, onExited, onDismiss, onSave, trayProps, audioOptions, requestSubtitlesFromIframe, }: {
1
+ declare function AudioOptionsTray({ open, onEntered, onExited, onDismiss, onSave, trayProps, audioOptions, requestSubtitlesFromIframe, onCaptionsModified, isLoading, id, }: {
2
2
  open: any;
3
3
  onEntered: any;
4
4
  onExited: any;
@@ -7,6 +7,9 @@ declare function AudioOptionsTray({ open, onEntered, onExited, onDismiss, onSave
7
7
  trayProps: any;
8
8
  audioOptions: any;
9
9
  requestSubtitlesFromIframe: any;
10
+ onCaptionsModified: any;
11
+ isLoading?: boolean | undefined;
12
+ id?: string | undefined;
10
13
  }): React.JSX.Element;
11
14
  declare namespace AudioOptionsTray {
12
15
  namespace propTypes {
@@ -26,7 +29,13 @@ declare namespace AudioOptionsTray {
26
29
  tracks: import("prop-types").Requireable<(import("prop-types").InferProps<{
27
30
  locale: import("prop-types").Validator<string>;
28
31
  }> | null | undefined)[]>;
32
+ viewerRestrictions: import("prop-types").Requireable<import("prop-types").InferProps<{
33
+ show_rolling_transcript: import("prop-types").Requireable<boolean>;
34
+ }>>;
29
35
  }>>>;
36
+ export { func as onCaptionsModified };
37
+ export { bool as isLoading };
38
+ export { string as id };
30
39
  }
31
40
  namespace defaultProps {
32
41
  let onEntered: null;
@@ -34,8 +43,11 @@ declare namespace AudioOptionsTray {
34
43
  let onDismiss: null;
35
44
  let onSave: null;
36
45
  function requestSubtitlesFromIframe(): void;
46
+ let onCaptionsModified: null;
37
47
  }
38
48
  }
39
49
  export default AudioOptionsTray;
40
50
  import React from 'react';
41
51
  import { func } from 'prop-types';
52
+ import { bool } from 'prop-types';
53
+ import { string } from 'prop-types';
@@ -16,20 +16,36 @@
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, { useCallback, useEffect, useRef, useState } from 'react';
20
20
  import { arrayOf, bool, func, shape, string } from 'prop-types';
21
+ import { ClosedCaptionPanel, ClosedCaptionPanelV2, CONSTANTS, trackPendoEvent, AUDIO_PLAYER_SIZE } from '@instructure/canvas-media';
22
+ import { Button, CloseButton } from '@instructure/ui-buttons';
23
+ import { Checkbox, CheckboxGroup } from '@instructure/ui-checkbox';
21
24
  import { Flex } from '@instructure/ui-flex';
22
- import { Tray } from '@instructure/ui-tray';
23
25
  import { FormFieldGroup } from '@instructure/ui-form-field';
24
- import { ClosedCaptionPanel } from '@instructure/canvas-media';
25
- import { Button, CloseButton } from '@instructure/ui-buttons';
26
- import { StoreProvider } from '../../shared/StoreContext';
26
+ import { Heading } from '@instructure/ui-heading';
27
+ import { IconExternalLinkLine } from '@instructure/ui-icons';
28
+ import { Link } from '@instructure/ui-link';
29
+ import { Spinner } from '@instructure/ui-spinner';
30
+ import { Tooltip } from '@instructure/ui-tooltip';
31
+ import { Tray } from '@instructure/ui-tray';
32
+ import { View } from '@instructure/ui-view';
33
+ import { SimpleSelect } from '@instructure/ui-simple-select';
34
+ import { Text } from '@instructure/ui-text';
27
35
  import Bridge from '../../../../bridge';
28
36
  import formatMessage from '../../../../format-message';
29
- import { getTrayHeight } from '../../shared/trayUtils';
37
+ import RCEGlobals from '../../../../rce/RCEGlobals';
38
+ import RceApiSource, { originFromHost } from '../../../../rcs/api';
30
39
  import { instuiPopupMountNodeFn } from '../../../../util/fullscreenHelpers';
31
- import { Heading } from '@instructure/ui-heading';
40
+ import { StoreProvider } from '../../shared/StoreContext';
41
+ import { getTrayHeight } from '../../shared/trayUtils';
42
+ import { mapViewerRestrictions, readViewerRestrictions } from '../utils';
43
+ import DimensionsInput, { useDimensionsState } from '../../shared/DimensionsInput';
44
+ import { getPlayerLayoutSizes, labelForPlayerLayoutSize, playerLayoutDimensions, scalePlayerLayoutForHeight, scalePlayerLayoutForWidth } from '../playerLayoutOptions';
45
+ import { CUSTOM, MIN_PERCENTAGE } from '../../instructure_image/ImageEmbedOptions';
32
46
  const getLiveRegion = () => document.getElementById('flash_screenreader_holder');
47
+ const MIN_WIDTH = AUDIO_PLAYER_SIZE.width;
48
+ const MIN_HEIGHT = AUDIO_PLAYER_SIZE.height;
33
49
  export default function AudioOptionsTray({
34
50
  open,
35
51
  onEntered,
@@ -38,20 +54,83 @@ export default function AudioOptionsTray({
38
54
  onSave,
39
55
  trayProps,
40
56
  audioOptions,
41
- requestSubtitlesFromIframe
57
+ requestSubtitlesFromIframe,
58
+ onCaptionsModified,
59
+ isLoading = false,
60
+ id = 'audio-options-tray'
42
61
  }) {
43
62
  const [subtitles, setSubtitles] = useState(audioOptions.tracks || []);
63
+ const api = new RceApiSource(trayProps);
64
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
65
+ const fetchedFromIframeRef = useRef(false);
66
+ const [viewerRestrictions, setViewerRestrictions] = useState(() => readViewerRestrictions(audioOptions.viewerRestrictions));
67
+ const [sizeKey, setSizeKey] = useState(() => {
68
+ var _match$;
69
+ const match = Object.entries(playerLayoutDimensions).find(([, dims]) => dims.width === audioOptions.containerDimensions.width);
70
+ return (_match$ = match?.[0]) !== null && _match$ !== void 0 ? _match$ : CUSTOM;
71
+ });
72
+ const dimensionsState = useDimensionsState({
73
+ appliedHeight: audioOptions.containerDimensions.height,
74
+ appliedWidth: audioOptions.containerDimensions.width,
75
+ usePercentageUnits: false
76
+ }, {
77
+ minHeight: MIN_HEIGHT,
78
+ minWidth: MIN_WIDTH,
79
+ minPercentage: MIN_PERCENTAGE
80
+ }, {
81
+ scaleFns: {
82
+ width: scalePlayerLayoutForWidth,
83
+ height: scalePlayerLayoutForHeight
84
+ }
85
+ });
44
86
  useEffect(() => {
45
- if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles);
46
- }, []);
47
- const handleSave = (e, contentProps) => {
87
+ if (!isLoading && subtitles.length === 0 && !fetchedFromIframeRef.current) {
88
+ // only request subtitle data after mount
89
+ fetchedFromIframeRef.current = true;
90
+ requestSubtitlesFromIframe(setSubtitles);
91
+ }
92
+ }, [isLoading, subtitles.length, requestSubtitlesFromIframe]);
93
+ const isAsrCaptioningImprovements = RCEGlobals.getFeatures()?.rce_asr_captioning_improvements;
94
+ useEffect(() => {
95
+ if (open && isAsrCaptioningImprovements) {
96
+ trackPendoEvent('canvas_media_options_opened', {
97
+ entry_point: 'quick_menu',
98
+ media_kind: 'audio'
99
+ });
100
+ }
101
+ }, [open, isAsrCaptioningImprovements]);
102
+ const handleUpdateSubtitles = newSubtitles => {
103
+ setSubtitles(newSubtitles);
104
+ };
105
+ const handleSave = (_e, contentProps) => {
48
106
  onSave({
49
107
  media_object_id: audioOptions.id,
50
108
  subtitles,
51
109
  attachment_id: audioOptions.attachmentId,
52
- updateMediaObject: contentProps.updateMediaObject
110
+ updateMediaObject: contentProps.updateMediaObject,
111
+ viewerRestrictions: mapViewerRestrictions(viewerRestrictions),
112
+ appliedHeight: sizeKey === CUSTOM ? dimensionsState.height : playerLayoutDimensions[sizeKey].height,
113
+ appliedWidth: sizeKey === CUSTOM ? dimensionsState.width : playerLayoutDimensions[sizeKey].width
53
114
  });
54
115
  };
116
+ const handleDirtyCheck = isDirty => {
117
+ setHasUnsavedChanges(isDirty);
118
+ };
119
+ const handleSizeChange = (_event, selectedOption) => {
120
+ setSizeKey(selectedOption.value);
121
+ };
122
+ const showSizeControls = isAsrCaptioningImprovements;
123
+ const applyDescribedBy = useCallback(playerLayoutInput => {
124
+ if (isAsrCaptioningImprovements && playerLayoutInput) {
125
+ const helperId = `${id}-size-helper-text`;
126
+ const existing = playerLayoutInput.getAttribute('aria-describedby') || '';
127
+ const ids = existing.split(' ').filter(Boolean);
128
+ if (!ids.includes(helperId)) {
129
+ playerLayoutInput.setAttribute('aria-describedby', [...ids, helperId].join(' '));
130
+ }
131
+ }
132
+ }, [isAsrCaptioningImprovements, id]);
133
+ const saveDisabled = sizeKey === CUSTOM && !dimensionsState.isValid;
55
134
  return /*#__PURE__*/React.createElement(StoreProvider, trayProps, contentProps => /*#__PURE__*/React.createElement(Tray, {
56
135
  key: "audio-options-tray",
57
136
  "data-mce-component": true,
@@ -64,7 +143,8 @@ export default function AudioOptionsTray({
64
143
  placement: "end",
65
144
  shouldCloseOnDocumentClick: true,
66
145
  shouldContainFocus: true,
67
- shouldReturnFocus: true
146
+ shouldReturnFocus: true,
147
+ size: isAsrCaptioningImprovements ? 'regular' : 'small'
68
148
  }, /*#__PURE__*/React.createElement(Flex, {
69
149
  direction: "column",
70
150
  height: getTrayHeight()
@@ -83,7 +163,13 @@ export default function AudioOptionsTray({
83
163
  color: "primary",
84
164
  onClick: onDismiss,
85
165
  screenReaderLabel: formatMessage('Close')
86
- })))), /*#__PURE__*/React.createElement(Flex.Item, {
166
+ })))), isLoading ? /*#__PURE__*/React.createElement(Flex.Item, {
167
+ textAlign: "center",
168
+ margin: "xx-large",
169
+ padding: "xx-large"
170
+ }, /*#__PURE__*/React.createElement(Spinner, {
171
+ renderTitle: formatMessage('Loading')
172
+ })) : /*#__PURE__*/React.createElement(Flex.Item, {
87
173
  as: "form",
88
174
  shouldGrow: true,
89
175
  margin: "none",
@@ -98,30 +184,129 @@ export default function AudioOptionsTray({
98
184
  shouldShrink: true
99
185
  }, /*#__PURE__*/React.createElement(Flex, {
100
186
  direction: "column"
101
- }, /*#__PURE__*/React.createElement(Flex.Item, {
187
+ }, showSizeControls && /*#__PURE__*/React.createElement(Flex.Item, {
188
+ margin: "small none xx-small none"
189
+ }, /*#__PURE__*/React.createElement(View, {
190
+ as: "div",
191
+ padding: "small small xx-small small"
192
+ }, /*#__PURE__*/React.createElement(SimpleSelect, {
193
+ inputRef: applyDescribedBy,
194
+ id: `${id}-size`,
195
+ mountNode: instuiPopupMountNodeFn,
196
+ renderLabel: formatMessage('Player layout'),
197
+ assistiveText: formatMessage('Use arrow keys to navigate options.'),
198
+ onChange: handleSizeChange,
199
+ value: sizeKey
200
+ }, getPlayerLayoutSizes().map(size => /*#__PURE__*/React.createElement(SimpleSelect.Option, {
201
+ id: `${id}-size-${size}`,
202
+ key: size,
203
+ value: size
204
+ }, labelForPlayerLayoutSize(size)))), /*#__PURE__*/React.createElement(View, {
205
+ as: "div",
206
+ id: `${id}-size-helper-text`,
207
+ margin: "xx-small none none none"
208
+ }, /*#__PURE__*/React.createElement(Text, {
209
+ size: "small"
210
+ }, formatMessage('Transcript panel is available at widths above 720px.')))), sizeKey === CUSTOM && /*#__PURE__*/React.createElement(View, {
211
+ as: "div",
212
+ padding: "xx-small small"
213
+ }, /*#__PURE__*/React.createElement(DimensionsInput, {
214
+ dimensionsState: dimensionsState,
215
+ minHeight: MIN_HEIGHT,
216
+ minWidth: MIN_WIDTH,
217
+ minPercentage: MIN_PERCENTAGE,
218
+ hidePercentage: true
219
+ }))), isAsrCaptioningImprovements && /*#__PURE__*/React.createElement(Flex.Item, {
220
+ padding: "small"
221
+ }, /*#__PURE__*/React.createElement(CheckboxGroup, {
222
+ name: "viewer-restrictions",
223
+ onChange: setViewerRestrictions,
224
+ defaultValue: viewerRestrictions,
225
+ description: /*#__PURE__*/React.createElement(Heading, {
226
+ level: "h4",
227
+ as: "h3"
228
+ }, formatMessage('Viewer Restrictions'))
229
+ }, /*#__PURE__*/React.createElement(Checkbox, {
230
+ variant: "toggle",
231
+ label: formatMessage('Show Rolling Transcript'),
232
+ value: "show_rolling_transcript"
233
+ }))), /*#__PURE__*/React.createElement(Flex.Item, {
102
234
  padding: "small"
103
235
  }, /*#__PURE__*/React.createElement(FormFieldGroup, {
104
- description: formatMessage('Closed Captions/Subtitles')
105
- }, /*#__PURE__*/React.createElement(ClosedCaptionPanel, {
236
+ description: /*#__PURE__*/React.createElement(Heading, {
237
+ level: "h4",
238
+ as: "h3"
239
+ }, formatMessage('Closed Captions/Subtitles'))
240
+ }, !isAsrCaptioningImprovements ? /*#__PURE__*/React.createElement(ClosedCaptionPanel, {
241
+ key: subtitles.reduce((acc, track) => acc + track.locale, ''),
106
242
  subtitles: subtitles.map(st => ({
107
243
  locale: st.locale,
108
244
  file: {
109
245
  name: st.language || st.locale
110
- }
246
+ },
247
+ asr: Boolean(st.asr)
111
248
  })),
112
249
  uploadMediaTranslations: Bridge.uploadMediaTranslations,
113
250
  languages: Bridge.languages,
114
251
  updateSubtitles: newSubtitles => setSubtitles(newSubtitles),
115
252
  liveRegion: getLiveRegion
116
- }))))), /*#__PURE__*/React.createElement(Flex.Item, {
253
+ }) : /*#__PURE__*/React.createElement(ClosedCaptionPanelV2, {
254
+ subtitles: subtitles.map(st => ({
255
+ ...st,
256
+ file: {
257
+ name: st.language || st.locale
258
+ },
259
+ asr: Boolean(st.asr)
260
+ })),
261
+ languages: Bridge.languages,
262
+ userLocale: Bridge.userLocale,
263
+ onUpdateSubtitles: handleUpdateSubtitles,
264
+ liveRegion: getLiveRegion,
265
+ mountNode: instuiPopupMountNodeFn,
266
+ uploadConfig: {
267
+ mediaObjectId: audioOptions.id,
268
+ attachmentId: audioOptions.attachmentId,
269
+ origin: originFromHost(api.host),
270
+ headers: api.jwt ? {
271
+ Authorization: `Bearer ${api.jwt}`
272
+ } : undefined,
273
+ maxBytes: CONSTANTS.CC_FILE_MAX_BYTES
274
+ },
275
+ onCaptionUploaded: subtitle => {
276
+ // Update local state so "Done" button knows about it
277
+ setSubtitles(prev => [...prev.filter(s => s.locale !== subtitle.locale), subtitle]);
278
+ onCaptionsModified?.();
279
+ },
280
+ onCaptionDeleted: locale => {
281
+ setSubtitles(prev => prev.filter(s => s.locale !== locale));
282
+ onCaptionsModified?.();
283
+ },
284
+ onDirtyStateChanged: handleDirtyCheck
285
+ }))), isAsrCaptioningImprovements ? /*#__PURE__*/React.createElement(Flex.Item, {
286
+ padding: "small"
287
+ }, /*#__PURE__*/React.createElement(Link, {
288
+ id: "tray-transcript-help-link",
289
+ variant: "standalone",
290
+ renderIcon: /*#__PURE__*/React.createElement(IconExternalLinkLine, null),
291
+ href: "https://productmarketing.instructuremedia.com/embed/32388c5a-580c-40f0-85a2-6b4042ddcccb",
292
+ target: "_blank",
293
+ rel: "noopener noreferrer"
294
+ }, formatMessage('How to request and edit captions?'))) : null)), /*#__PURE__*/React.createElement(Flex.Item, {
117
295
  background: "secondary",
118
296
  borderWidth: "small none none none",
119
297
  padding: "small medium",
120
298
  textAlign: "end"
299
+ }, /*#__PURE__*/React.createElement(Tooltip, {
300
+ renderTip: formatMessage('Unsaved changes will be lost.'),
301
+ placement: "top",
302
+ on: ['hover', 'focus'],
303
+ preventTooltip: !hasUnsavedChanges,
304
+ mountNode: instuiPopupMountNodeFn
121
305
  }, /*#__PURE__*/React.createElement(Button, {
122
306
  onClick: e => handleSave(e, contentProps),
123
- color: "primary"
124
- }, formatMessage('Done'))))))));
307
+ color: "primary",
308
+ interaction: saveDisabled ? 'disabled' : 'enabled'
309
+ }, formatMessage('Done')))))))));
125
310
  }
126
311
  AudioOptionsTray.propTypes = {
127
312
  onEntered: func,
@@ -139,13 +324,20 @@ AudioOptionsTray.propTypes = {
139
324
  titleText: string.isRequired,
140
325
  tracks: arrayOf(shape({
141
326
  locale: string.isRequired
142
- }))
143
- }).isRequired
327
+ })),
328
+ viewerRestrictions: shape({
329
+ show_rolling_transcript: bool
330
+ })
331
+ }).isRequired,
332
+ onCaptionsModified: func,
333
+ isLoading: bool,
334
+ id: string
144
335
  };
145
336
  AudioOptionsTray.defaultProps = {
146
337
  onEntered: null,
147
338
  onExited: null,
148
339
  onDismiss: null,
149
340
  onSave: null,
150
- requestSubtitlesFromIframe: () => {}
341
+ requestSubtitlesFromIframe: () => {},
342
+ onCaptionsModified: null
151
343
  };
@@ -16,14 +16,16 @@
16
16
  * with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
18
 
19
- import React, { useRef } from 'react';
20
- import { func, oneOf, shape, string } from 'prop-types';
21
- import { contentTrayDocumentShape } from '../../shared/fileShape';
22
- import formatMessage from '../../../../format-message';
19
+ import { trackPendoEvent } from '@instructure/canvas-media';
23
20
  import { Text } from '@instructure/ui-text';
24
21
  import { View } from '@instructure/ui-view';
22
+ import { func, oneOf, shape, string } from 'prop-types';
23
+ import React, { useRef } from 'react';
24
+ import { LoadingIndicator, LoadingStatus, LoadMoreButton, useIncrementalLoading } from '../../../../common/incremental-loading';
25
+ import formatMessage from '../../../../format-message';
26
+ import RCEGlobals from '../../../RCEGlobals';
25
27
  import Link from '../../instructure_documents/components/Link';
26
- import { LoadMoreButton, LoadingIndicator, LoadingStatus, useIncrementalLoading } from '../../../../common/incremental-loading';
28
+ import { contentTrayDocumentShape } from '../../shared/fileShape';
27
29
  const PENDING_MEDIA_ENTRY_ID = 'maybe';
28
30
  function hasFiles(media) {
29
31
  return media.files.length > 0;
@@ -84,6 +86,15 @@ export default function MediaPanel(props) {
84
86
  searchString
85
87
  });
86
88
  const handleFileClick = file => {
89
+ if (RCEGlobals.getFeatures()?.rce_asr_captioning_improvements) {
90
+ const contentType = file.content_type || file['content-type'] || '';
91
+ trackPendoEvent('canvas_native_media_embedded', {
92
+ insertion_method: 'select_existing',
93
+ media_id: file.id,
94
+ media_kind: contentType.startsWith('audio/') ? 'audio' : 'video',
95
+ resourceType: props.contextType
96
+ });
97
+ }
87
98
  props.onMediaEmbed(file);
88
99
  };
89
100
  return /*#__PURE__*/React.createElement(View, {
@@ -1,19 +1,13 @@
1
1
  export const CONTAINER_ID: "instructure-video-options-tray-container";
2
- export namespace VIDEO_SIZE_DEFAULT {
2
+ export namespace STUDIO_PLAYER_VIDEO_SIZE_DEFAULT {
3
3
  let height: string;
4
4
  let width: string;
5
5
  }
6
- export namespace STUDIO_PLAYER_VIDEO_SIZE_DEFAULT {
7
- let height_1: string;
8
- export { height_1 as height };
6
+ export namespace AUDIO_PLAYER_SIZE {
9
7
  let width_1: string;
10
8
  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 };
9
+ let height_1: string;
10
+ export { height_1 as height };
17
11
  }
18
12
  export function videoDefaultSize(): {
19
13
  height: string;
@@ -24,14 +18,21 @@ export default class TrayController {
24
18
  _isOpen: boolean;
25
19
  _shouldOpen: boolean;
26
20
  _renderId: number;
21
+ _skipFocusOnExit: boolean;
22
+ _captionsModified: boolean;
23
+ requestSubtitlesFromIframe(cb: any): void;
24
+ isStudioVideo: boolean;
27
25
  get $container(): HTMLElement;
28
26
  get isOpen(): boolean;
29
27
  showTrayForEditor(editor: any): void;
30
28
  $videoContainer: Element | null | undefined;
31
- hideTrayForEditor(editor: any): void;
29
+ _isPlayerReady: boolean | undefined;
30
+ hideTrayForEditor(editor: any, skipFocusOnExit?: boolean): void;
32
31
  _applyVideoOptions(videoOptions: any): void;
32
+ _listenForPlayerIframeToLoad(currentMediaId: any): void;
33
+ _iframeLoadingListener: AbortController | undefined;
34
+ _reloadVideoPlayer(): void;
33
35
  _dismissTray(): void;
34
- requestSubtitlesFromIframe(cb: any): void;
35
36
  _subtitleListener: AbortController | undefined;
36
- _renderTray(trayProps: any): void;
37
+ _renderTray(): void;
37
38
  }