@instructure/canvas-rce 7.2.0 → 7.3.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 (97) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/es/enhance-user-content/doc_previews.js +1 -14
  3. package/es/index.d.ts +1 -0
  4. package/es/index.js +2 -1
  5. package/es/rce/AlertMessageArea.d.ts +2 -2
  6. package/es/rce/AlertMessageArea.js +4 -6
  7. package/es/rce/RCEGlobals.d.ts +2 -0
  8. package/es/rce/RCEGlobals.js +1 -0
  9. package/es/rce/RCEVariants.js +3 -3
  10. package/es/rce/RCEWrapper.d.ts +14 -12
  11. package/es/rce/RCEWrapper.js +206 -199
  12. package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.d.ts +2 -0
  13. package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.js +45 -0
  14. package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.d.ts +1 -0
  15. package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.js +43 -0
  16. package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.d.ts +1 -1
  17. package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.js +25 -25
  18. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.d.ts +1 -1
  19. package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.js +2 -1
  20. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.d.ts +1 -1
  21. package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.js +2 -1
  22. package/es/rce/plugins/instructure_record/mediaTranslations.js +1 -1
  23. package/es/rce/plugins/instructure_studio_media_options/plugin.js +109 -14
  24. package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.d.ts +5 -0
  25. package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.js +23 -0
  26. package/es/rce/plugins/instructure_wordcount_header/plugin.d.ts +1 -0
  27. package/es/rce/plugins/instructure_wordcount_header/plugin.js +75 -0
  28. package/es/rce/plugins/shared/ContentSelection.d.ts +1 -2
  29. package/es/rce/plugins/shared/ContentSelection.js +1 -18
  30. package/es/rce/plugins/shared/StudioLtiSupportUtils.d.ts +9 -1
  31. package/es/rce/plugins/shared/StudioLtiSupportUtils.js +94 -1
  32. package/es/rce/plugins/shared/Upload/ComputerPanel.js +1 -1
  33. package/es/rce/plugins/shared/Upload/UploadFileModal.js +37 -4
  34. package/es/rce/plugins/shared/Upload/VideoUrlPanel.d.ts +15 -0
  35. package/es/rce/plugins/shared/Upload/VideoUrlPanel.js +51 -0
  36. package/es/rce/plugins/shared/Upload/videoValidationUtils.d.ts +7 -0
  37. package/es/rce/plugins/shared/Upload/videoValidationUtils.js +58 -0
  38. package/es/rce/plugins/shared/iframeUtils.d.ts +1 -0
  39. package/es/rce/plugins/shared/iframeUtils.js +37 -0
  40. package/es/rce/tinyRCE.js +2 -0
  41. package/es/sidebar/actions/upload.d.ts +1 -0
  42. package/es/sidebar/actions/upload.js +56 -0
  43. package/es/sidebar/containers/sidebarHandlers.d.ts +1 -0
  44. package/es/sidebar/containers/sidebarHandlers.js +2 -1
  45. package/es/translations/locales/ar.js +15 -6
  46. package/es/translations/locales/ca.js +15 -6
  47. package/es/translations/locales/cy.js +15 -6
  48. package/es/translations/locales/da-x-k12.js +15 -6
  49. package/es/translations/locales/da.js +15 -6
  50. package/es/translations/locales/de.js +15 -6
  51. package/es/translations/locales/el.js +6 -0
  52. package/es/translations/locales/en-AU-x-unimelb.js +15 -6
  53. package/es/translations/locales/en-GB-x-ukhe.js +15 -6
  54. package/es/translations/locales/en.js +30 -6
  55. package/es/translations/locales/en_AU.js +15 -6
  56. package/es/translations/locales/en_CA.js +15 -6
  57. package/es/translations/locales/en_CY.js +15 -6
  58. package/es/translations/locales/en_GB.js +15 -6
  59. package/es/translations/locales/es.js +15 -6
  60. package/es/translations/locales/es_ES.js +15 -6
  61. package/es/translations/locales/fa_IR.js +6 -3
  62. package/es/translations/locales/fi.js +15 -6
  63. package/es/translations/locales/fr.js +15 -6
  64. package/es/translations/locales/fr_CA.js +19 -10
  65. package/es/translations/locales/ga.js +15 -6
  66. package/es/translations/locales/he.js +6 -0
  67. package/es/translations/locales/hi.js +15 -6
  68. package/es/translations/locales/ht.js +15 -6
  69. package/es/translations/locales/hu.js +6 -6
  70. package/es/translations/locales/hy.js +6 -0
  71. package/es/translations/locales/id.js +15 -6
  72. package/es/translations/locales/is.js +21 -6
  73. package/es/translations/locales/it.js +15 -6
  74. package/es/translations/locales/ja.js +15 -6
  75. package/es/translations/locales/ko.js +6 -0
  76. package/es/translations/locales/mi.js +15 -6
  77. package/es/translations/locales/ms.js +15 -6
  78. package/es/translations/locales/nb-x-k12.js +15 -6
  79. package/es/translations/locales/nb.js +15 -6
  80. package/es/translations/locales/nl.js +15 -6
  81. package/es/translations/locales/nn.js +6 -6
  82. package/es/translations/locales/pl.js +15 -6
  83. package/es/translations/locales/pt.js +15 -6
  84. package/es/translations/locales/pt_BR.js +15 -6
  85. package/es/translations/locales/ru.js +15 -6
  86. package/es/translations/locales/sl.js +15 -6
  87. package/es/translations/locales/sv-x-k12.js +15 -6
  88. package/es/translations/locales/sv.js +15 -6
  89. package/es/translations/locales/th.js +15 -6
  90. package/es/translations/locales/tr.js +6 -3
  91. package/es/translations/locales/uk_UA.js +6 -3
  92. package/es/translations/locales/vi.js +15 -6
  93. package/es/translations/locales/zh-Hans.js +15 -6
  94. package/es/translations/locales/zh-Hant.js +15 -6
  95. package/es/translations/locales/zh.js +15 -6
  96. package/es/translations/locales/zh_HK.js +15 -6
  97. package/package.json +53 -53
@@ -0,0 +1,2 @@
1
+ import { Editor } from 'tinymce';
2
+ export default function (ed: Editor, document: Document): Promise<void>;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (C) 2024 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import React from 'react';
20
+ import ReactDOM from 'react-dom';
21
+ const MODAL_ID = 'canvas-rce-keyboard-shortcuts-container';
22
+ export default function (ed, document) {
23
+ return import('../../KeyboardShortcutModal').then(({
24
+ default: KeyboardShortcutModal
25
+ }) => {
26
+ let container = document.querySelector(`#${MODAL_ID}`);
27
+ if (!container) {
28
+ container = document.createElement('div');
29
+ container.id = MODAL_ID;
30
+ document.body.appendChild(container);
31
+ }
32
+ const handleDismiss = () => {
33
+ if (container) {
34
+ ReactDOM.unmountComponentAtNode(container);
35
+ }
36
+ ed.focus();
37
+ };
38
+ ReactDOM.render(/*#__PURE__*/React.createElement(KeyboardShortcutModal, {
39
+ open: true,
40
+ onClose: handleDismiss,
41
+ onDismiss: handleDismiss,
42
+ onExited: handleDismiss
43
+ }), container);
44
+ });
45
+ }
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright (C) 2024 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import formatMessage from '../../../format-message';
20
+ // @ts-expect-error
21
+ import { IconKeyboardShortcutsLine } from '@instructure/ui-icons/es/svg';
22
+
23
+ // Dynamically import the callback to avoid module resolution issues
24
+ const clickCallbackPromise = import('./clickCallback');
25
+
26
+ // @ts-expect-error: tinymce is available as a global variable
27
+ tinymce.PluginManager.add('instructure_keyboard_shortcuts_header', function (ed) {
28
+ // Register custom icon
29
+ ed.ui.registry.addIcon('keyboard-shortcuts', IconKeyboardShortcutsLine.src);
30
+ ed.addCommand('instructureKeyboardShortcuts', () => {
31
+ clickCallbackPromise.then(module => module.default(ed, document));
32
+ });
33
+ ed.ui.registry.addButton('instructure_keyboard_shortcuts_header', {
34
+ icon: 'keyboard-shortcuts',
35
+ tooltip: formatMessage('View keyboard shortcuts'),
36
+ onAction: () => ed.execCommand('instructureKeyboardShortcuts')
37
+ });
38
+ ed.ui.registry.addMenuItem('instructure_keyboard_shortcuts_header', {
39
+ icon: 'keyboard-shortcuts',
40
+ text: formatMessage('Keyboard Shortcuts'),
41
+ onAction: () => ed.execCommand('instructureKeyboardShortcuts')
42
+ });
43
+ });
@@ -1,4 +1,4 @@
1
- import { RceToolWrapper } from '../../RceToolWrapper';
1
+ import { RceToolWrapper } from "../../RceToolWrapper";
2
2
  /**
3
3
  * Returns a filtered list of items based on the term.
4
4
  *
@@ -17,22 +17,22 @@ import _pt from "prop-types";
17
17
  * with this program. If not, see <http://www.gnu.org/licenses/>.
18
18
  */
19
19
 
20
- import React, { useState } from 'react';
21
- import { Modal } from '@instructure/ui-modal';
22
- import { Button, CloseButton } from '@instructure/ui-buttons';
23
- import { Heading } from '@instructure/ui-heading';
24
- import { View } from '@instructure/ui-view';
25
- import { ScreenReaderContent } from '@instructure/ui-a11y-content';
26
- import { List } from '@instructure/ui-list';
27
- import { Flex } from '@instructure/ui-flex';
28
- import { TextInput } from '@instructure/ui-text-input';
29
- import { IconSearchLine } from '@instructure/ui-icons';
30
- import { Alert } from '@instructure/ui-alerts';
31
- import formatMessage from '../../../../../format-message';
32
- import ExternalToolSelectionItem from './ExternalToolSelectionItem';
33
- import { instuiPopupMountNodeFn } from '../../../../../util/fullscreenHelpers';
20
+ import React, { useState } from "react";
21
+ import { Modal } from "@instructure/ui-modal";
22
+ import { Button, CloseButton } from "@instructure/ui-buttons";
23
+ import { Heading } from "@instructure/ui-heading";
24
+ import { View } from "@instructure/ui-view";
25
+ import { ScreenReaderContent } from "@instructure/ui-a11y-content";
26
+ import { List } from "@instructure/ui-list";
27
+ import { Flex } from "@instructure/ui-flex";
28
+ import { TextInput } from "@instructure/ui-text-input";
29
+ import { IconSearchLine } from "@instructure/ui-icons";
30
+ import { Alert } from "@instructure/ui-alerts";
31
+ import formatMessage from "../../../../../format-message";
32
+ import ExternalToolSelectionItem from "./ExternalToolSelectionItem";
33
+ import { instuiPopupMountNodeFn } from "../../../../../util/fullscreenHelpers";
34
34
  // TODO: we really need a way for the client to pass this to the RCE
35
- const getLiveRegion = () => document.getElementById('flash_screenreader_holder');
35
+ const getLiveRegion = () => document.getElementById("flash_screenreader_holder");
36
36
 
37
37
  /**
38
38
  * Returns a filtered list of items based on the term.
@@ -51,7 +51,7 @@ export function filterItemsByTitleSubstring(searchString, items) {
51
51
  return items.filter(item => item.title.toLocaleLowerCase().includes(lowerTerm));
52
52
  }
53
53
  export function ExternalToolSelectionDialog(props) {
54
- const [filterTerm, setFilterTerm] = useState('');
54
+ const [filterTerm, setFilterTerm] = useState("");
55
55
  const [filteredResults, setFilteredResults] = useState(props.ltiButtons);
56
56
  const handleFilterChange = e => {
57
57
  setFilterTerm(e.target?.value);
@@ -63,31 +63,31 @@ export function ExternalToolSelectionDialog(props) {
63
63
  liveRegion: getLiveRegion,
64
64
  size: "medium",
65
65
  themeOverride: {
66
- mediumMaxWidth: '42rem'
66
+ mediumMaxWidth: "42rem"
67
67
  },
68
- label: formatMessage('Apps'),
68
+ label: formatMessage("Apps"),
69
69
  mountNode: instuiPopupMountNodeFn(),
70
70
  onDismiss: props.onDismiss,
71
71
  open: true,
72
72
  shouldCloseOnDocumentClick: false
73
73
  }, /*#__PURE__*/React.createElement(Modal.Header, {
74
74
  themeOverride: {
75
- padding: '0.5rem'
75
+ padding: "0.5rem"
76
76
  }
77
77
  }, /*#__PURE__*/React.createElement(CloseButton, {
78
78
  placement: "end",
79
79
  offset: "medium",
80
80
  onClick: props.onDismiss,
81
- screenReaderLabel: formatMessage('Close')
81
+ screenReaderLabel: formatMessage("Close")
82
82
  }), /*#__PURE__*/React.createElement(Heading, {
83
83
  margin: "small"
84
- }, formatMessage('All Apps')), /*#__PURE__*/React.createElement(View, {
84
+ }, formatMessage("All Apps")), /*#__PURE__*/React.createElement(View, {
85
85
  as: "div",
86
86
  padding: "x-small none x-small medium"
87
87
  }, /*#__PURE__*/React.createElement(TextInput, {
88
88
  type: "search",
89
- renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Search')),
90
- placeholder: formatMessage('Search'),
89
+ renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage("Search")),
90
+ placeholder: formatMessage("Search"),
91
91
  renderAfterInput: /*#__PURE__*/React.createElement(IconSearchLine, {
92
92
  inline: false
93
93
  }),
@@ -105,7 +105,7 @@ export function ExternalToolSelectionDialog(props) {
105
105
  liveRegion: getLiveRegion,
106
106
  variant: "info",
107
107
  screenReaderOnly: !filterEmpty
108
- }, filterEmpty && formatMessage('No results found for {filterTerm}', {
108
+ }, filterEmpty && formatMessage("No results found for {filterTerm}", {
109
109
  filterTerm
110
110
  }), !filterEmpty && formatMessage(`Found { count, plural,
111
111
  =0 {# results}
@@ -116,7 +116,7 @@ export function ExternalToolSelectionDialog(props) {
116
116
  })), renderTools(filteredResults)))), /*#__PURE__*/React.createElement(Modal.Footer, null, /*#__PURE__*/React.createElement(Button, {
117
117
  onClick: props.onDismiss,
118
118
  color: "primary"
119
- }, formatMessage('Done'))));
119
+ }, formatMessage("Done"))));
120
120
  function renderTools(ltiButtons) {
121
121
  return /*#__PURE__*/React.createElement(List, {
122
122
  isUnstyled: true
@@ -3,7 +3,7 @@ export default class TrayController {
3
3
  _isOpen: boolean;
4
4
  _shouldOpen: boolean;
5
5
  _editor: any;
6
- _audioContainer: any;
6
+ _audioContainer: Element | null;
7
7
  get container(): HTMLElement;
8
8
  get isOpen(): boolean;
9
9
  showTrayForEditor(editor: any): void;
@@ -19,7 +19,8 @@
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
- import { asAudioElement, findMediaPlayerIframe } from '../../shared/ContentSelection';
22
+ import { asAudioElement } from '../../shared/ContentSelection';
23
+ import { findMediaPlayerIframe } from '../../shared/iframeUtils';
23
24
  import AudioOptionsTray from '.';
24
25
  export const CONTAINER_ID = 'instructure-audio-options-tray-container';
25
26
  export default class TrayController {
@@ -27,7 +27,7 @@ export default class TrayController {
27
27
  get $container(): HTMLElement;
28
28
  get isOpen(): boolean;
29
29
  showTrayForEditor(editor: any): void;
30
- $videoContainer: any;
30
+ $videoContainer: Element | null | undefined;
31
31
  hideTrayForEditor(editor: any): void;
32
32
  _applyVideoOptions(videoOptions: any): void;
33
33
  _dismissTray(): void;
@@ -19,7 +19,8 @@
19
19
  import React from 'react';
20
20
  import ReactDOM from 'react-dom';
21
21
  import bridge from '../../../../bridge';
22
- import { asVideoElement, findMediaPlayerIframe } from '../../shared/ContentSelection';
22
+ import { asVideoElement } from '../../shared/ContentSelection';
23
+ import { findMediaPlayerIframe } from '../../shared/iframeUtils';
23
24
  import VideoOptionsTray from '.';
24
25
  import { isStudioEmbeddedMedia, parseStudioOptions } from '../../shared/StudioLtiSupportUtils';
25
26
  import RCEGlobals from '../../../RCEGlobals';
@@ -24,7 +24,7 @@ import formatMessage from '../../../format-message';
24
24
  const uploadMediaTranslations = {
25
25
  UploadMediaStrings: {
26
26
  ADD_CLOSED_CAPTIONS_OR_SUBTITLES: formatMessage('Add CC/Subtitles'),
27
- CLEAR_FILE_TEXT: formatMessage('Clear selected file'),
27
+ CLEAR_FILE_TEXT: formatMessage('Remove'),
28
28
  CLOSE_TEXT: formatMessage('Close'),
29
29
  CLOSED_CAPTIONS_CHOOSE_FILE: formatMessage('Choose caption file'),
30
30
  CLOSED_CAPTIONS_SELECT_LANGUAGE: formatMessage('Select Language'),
@@ -17,24 +17,119 @@
17
17
  */
18
18
 
19
19
  import tinymce from 'tinymce';
20
- import { isStudioEmbeddedMedia, handleBeforeObjectSelected } from '../shared/StudioLtiSupportUtils';
20
+ import { isStudioEmbeddedMedia, handleBeforeObjectSelected, notifyStudioEmbedTypeChange, updateStudioIframeDimensions, isValidDimension, isValidEmbedType } from '../shared/StudioLtiSupportUtils';
21
21
  import VideoTrayController from '../instructure_record/VideoOptionsTray/TrayController';
22
22
  import formatMessage from '../../../format-message';
23
+ import RCEGlobals from '../../RCEGlobals';
24
+ import { thumbnailViewIcon, learnViewIcon, collabViewIcon, optionsIcon, removeIcon } from './studioToolbarIcons';
23
25
  const studioTrayController = new VideoTrayController();
26
+ const handleStudioMessage = e => {
27
+ if (e.data && e.data.subject === 'studio.embedTypeChanged.response' && isValidDimension(e.data.width) && isValidDimension(e.data.height) && isValidEmbedType(e.data.embedType)) {
28
+ updateStudioIframeDimensions(tinymce.activeEditor, e.data.width, e.data.height, e.data.embedType);
29
+ }
30
+ };
24
31
  tinymce.PluginManager.add('instructure_studio_media_options', function (ed) {
25
- ed.ui.registry.addButton('studio-media-options', {
26
- onAction() {
27
- studioTrayController.showTrayForEditor(ed);
28
- },
29
- text: formatMessage('Studio Media Options'),
30
- tooltip: formatMessage('Show Studio media options')
31
- });
32
- ed.ui.registry.addContextToolbar('studio-media-options-toolbar', {
33
- items: 'studio-media-options',
34
- position: 'node',
35
- predicate: isStudioEmbeddedMedia,
36
- scope: 'node'
37
- });
32
+ if (RCEGlobals.getFeatures().rce_studio_embed_improvements) {
33
+ window.addEventListener('message', handleStudioMessage);
34
+ ed.on('init', () => {
35
+ const existingStyle = document.getElementById('studio-toolbar-styles');
36
+ if (!existingStyle) {
37
+ const style = document.createElement('style');
38
+ style.id = 'studio-toolbar-styles';
39
+ style.textContent = `
40
+ .tox .tox-pop .tox-tbtn {
41
+ font-size: 16px;
42
+ border-radius: 0;
43
+ }
44
+
45
+ .tox .tox-pop .tox-tbtn:hover {
46
+ background-color: #2B7ABC;
47
+ color: white;
48
+ }
49
+ `;
50
+ document.head.appendChild(style);
51
+ }
52
+ });
53
+ ed.ui.registry.addIcon('thumbnail-view-icon', thumbnailViewIcon);
54
+ ed.ui.registry.addIcon('learn-view-icon', learnViewIcon);
55
+ ed.ui.registry.addIcon('collab-view-icon', collabViewIcon);
56
+ ed.ui.registry.addIcon('options-icon', optionsIcon);
57
+ ed.ui.registry.addIcon('remove-icon', removeIcon);
58
+ ed.ui.registry.addButton('thumbnail-view', {
59
+ onAction() {
60
+ notifyStudioEmbedTypeChange(ed, 'thumbnail_embed');
61
+ },
62
+ icon: 'thumbnail-view-icon',
63
+ text: formatMessage('Thumbnail'),
64
+ tooltip: formatMessage('Thumbnail')
65
+ });
66
+ ed.ui.registry.addButton('learn-view', {
67
+ onAction() {
68
+ notifyStudioEmbedTypeChange(ed, 'learn_embed');
69
+ },
70
+ icon: 'learn-view-icon',
71
+ text: formatMessage('Learn'),
72
+ tooltip: formatMessage('Learn')
73
+ });
74
+ ed.ui.registry.addButton('collab-view', {
75
+ onAction() {
76
+ notifyStudioEmbedTypeChange(ed, 'collaboration_embed');
77
+ },
78
+ icon: 'collab-view-icon',
79
+ text: formatMessage('Collab'),
80
+ tooltip: formatMessage('Collab')
81
+ });
82
+ ed.ui.registry.addButton('studio-media-options', {
83
+ onAction() {
84
+ if (!studioTrayController.isOpen) {
85
+ studioTrayController.showTrayForEditor(ed);
86
+ }
87
+ },
88
+ icon: 'options-icon',
89
+ text: formatMessage('Options'),
90
+ tooltip: formatMessage('Options')
91
+ });
92
+ ed.ui.registry.addButton('remove-studio-media', {
93
+ onAction() {
94
+ const selectedElement = ed.selection.getNode();
95
+ if (selectedElement && isStudioEmbeddedMedia(selectedElement)) {
96
+ studioTrayController.hideTrayForEditor(ed);
97
+
98
+ // Hide toolbar, reset selection
99
+ ed.fire('hidecontexttoolbar');
100
+ ed.dom.remove(selectedElement);
101
+ ed.nodeChanged();
102
+ ed.selection.select(ed.getBody());
103
+
104
+ // Force focus back to editor
105
+ ed.focus();
106
+ }
107
+ },
108
+ icon: 'remove-icon',
109
+ text: formatMessage('Remove'),
110
+ tooltip: formatMessage('Remove Studio Media')
111
+ });
112
+ ed.ui.registry.addContextToolbar('studio-extra-toolbar', {
113
+ items: 'thumbnail-view | learn-view | collab-view | studio-media-options | remove-studio-media',
114
+ position: 'node',
115
+ predicate: isStudioEmbeddedMedia,
116
+ scope: 'node'
117
+ });
118
+ } else {
119
+ ed.ui.registry.addButton('studio-media-options', {
120
+ onAction() {
121
+ studioTrayController.showTrayForEditor(ed);
122
+ },
123
+ text: formatMessage('Studio Media Options'),
124
+ tooltip: formatMessage('Show Studio media options')
125
+ });
126
+ ed.ui.registry.addContextToolbar('studio-media-options-toolbar', {
127
+ items: 'studio-media-options',
128
+ position: 'node',
129
+ predicate: isStudioEmbeddedMedia,
130
+ scope: 'node'
131
+ });
132
+ }
38
133
  ed.on('BeforeObjectSelected', handleBeforeObjectSelected);
39
134
  ed.on('remove', editor => {
40
135
  studioTrayController.hideTrayForEditor(editor);
@@ -0,0 +1,5 @@
1
+ export declare const thumbnailViewIcon = "<svg width=\"24\" height=\"17\" viewBox=\"0 0 24 17\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g clip-path=\"url(#clip0_1209_167295)\"><path d=\"M24 17H0V0H24V17ZM1.10983 1.10569V15.8943H22.8902V1.10569H1.10983Z\" fill=\"currentColor\"/><path d=\"M21.7804 14.7887H2.21973V2.21143H21.7804V14.7887ZM9.84978 11.4716L14.5665 8.63826L9.84978 5.80492V11.4716Z\" fill=\"currentColor\"/></g><defs><clipPath id=\"clip0_1209_167295\"><rect width=\"24\" height=\"17\" fill=\"white\"/></clipPath></defs></svg>";
2
+ export declare const learnViewIcon = "<svg width=\"31\" height=\"22\" viewBox=\"0 0 31 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g clip-path=\"url(#clip0_93_30328)\"><path d=\"M31 22H0V0H31V22ZM1.43353 1.43089V20.5691H29.5665V1.43089H1.43353Z\" fill=\"currentColor\"/><path d=\"M27.9537 3.04102H19.7109V18.9597H27.9537V3.04102Z\" fill=\"currentColor\"/><path d=\"M18.2781 18.9597H3.04688V3.04102H18.2781V18.9597ZM8.02537 14.7989L14.3165 11.0907L8.02537 7.38244V14.7989Z\" fill=\"currentColor\"/></g><defs><clipPath id=\"clip0_93_30328\"><rect width=\"31\" height=\"22\" fill=\"white\"/></clipPath></defs></svg>";
3
+ export declare const collabViewIcon = "<svg width=\"24\" height=\"17\" viewBox=\"0 0 24 17\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g clip-path=\"url(#clip0_1209_167302)\"><path d=\"M24 17H0V0H24V17ZM1.10983 1.10569V15.8943H22.8902V1.10569H1.10983Z\" fill=\"currentColor\"/><path d=\"M21.6416 2.34961H17.3411V8.15449H21.6416V2.34961Z\" fill=\"currentColor\"/><path d=\"M16.2312 8.15449H2.3584V2.34961H16.2312V8.15449ZM8.60117 7.18701L11.7919 5.39026L8.60117 3.59351V7.18701Z\" fill=\"currentColor\"/><path d=\"M21.6415 12.5771H4.99414V13.4064H21.6415V12.5771Z\" fill=\"currentColor\"/><path d=\"M21.6415 13.8213H4.99414V14.6506H21.6415V13.8213Z\" fill=\"currentColor\"/><path d=\"M3.39886 14.6503C3.97349 14.6503 4.43932 14.1862 4.43932 13.6137C4.43932 13.0412 3.97349 12.5771 3.39886 12.5771C2.82423 12.5771 2.3584 13.0412 2.3584 13.6137C2.3584 14.1862 2.82423 14.6503 3.39886 14.6503Z\" fill=\"currentColor\"/><path d=\"M21.6415 9.39844H4.99414V10.2277H21.6415V9.39844Z\" fill=\"currentColor\"/><path d=\"M21.6415 10.6421H4.99414V11.4714H21.6415V10.6421Z\" fill=\"currentColor\"/><path d=\"M3.39886 11.4716C3.97349 11.4716 4.43932 11.0075 4.43932 10.435C4.43932 9.86253 3.97349 9.39844 3.39886 9.39844C2.82423 9.39844 2.3584 9.86253 2.3584 10.435C2.3584 11.0075 2.82423 11.4716 3.39886 11.4716Z\" fill=\"currentColor\"/></g><defs><clipPath id=\"clip0_1209_167302\"><rect width=\"24\" height=\"17\" fill=\"white\"/></clipPath></defs></svg>";
4
+ export declare const optionsIcon = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><g clip-path=\"url(#clip0_1217_47603)\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.46326 3.85039H0V2.65039H2.46326C2.7388 1.36449 3.88181 0.400391 5.24999 0.400391C6.61818 0.400391 7.76119 1.36449 8.03673 2.65039H18V3.85039H8.03673C7.76119 5.13629 6.61818 6.10039 5.24999 6.10039C3.88181 6.10039 2.7388 5.13629 2.46326 3.85039ZM3.59999 3.25039C3.59999 2.33912 4.33872 1.60039 5.24999 1.60039C6.16126 1.60039 6.89999 2.33912 6.89999 3.25039C6.89999 4.16166 6.16126 4.90039 5.24999 4.90039C4.33872 4.90039 3.59999 4.16166 3.59999 3.25039Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M0 8.40039H9.46326C9.7388 7.11449 10.8818 6.15039 12.25 6.15039C13.6182 6.15039 14.7612 7.11449 15.0367 8.40039H18V9.60039H15.0367C14.7612 10.8863 13.6182 11.8504 12.25 11.8504C10.8818 11.8504 9.7388 10.8863 9.46326 9.60039H0V8.40039ZM12.25 7.35039C11.3387 7.35039 10.6 8.08912 10.6 9.00039C10.6 9.91166 11.3387 10.6504 12.25 10.6504C13.1613 10.6504 13.9 9.91166 13.9 9.00039C13.9 8.08912 13.1613 7.35039 12.25 7.35039Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.24999 17.6004C6.61818 17.6004 7.76119 16.6363 8.03673 15.3504H18V14.1504H8.03673C7.76119 12.8645 6.61818 11.9004 5.24999 11.9004C3.88181 11.9004 2.7388 12.8645 2.46326 14.1504H0V15.3504H2.46326C2.7388 16.6363 3.88181 17.6004 5.24999 17.6004ZM5.24999 13.1004C4.33872 13.1004 3.59999 13.8391 3.59999 14.7504C3.59999 15.6617 4.33872 16.4004 5.24999 16.4004C6.16126 16.4004 6.89999 15.6617 6.89999 14.7504C6.89999 13.8391 6.16126 13.1004 5.24999 13.1004Z\" fill=\"currentColor\"/></g><defs><clipPath id=\"clip0_1217_47603\"><rect width=\"18\" height=\"18\" fill=\"white\"/></clipPath></defs></svg>";
5
+ export declare const removeIcon = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.8234 16.4118C14.8234 16.7029 14.5852 16.9412 14.294 16.9412H3.70577C3.41459 16.9412 3.17636 16.7029 3.17636 16.4118V5.29412H2.11753V16.4118C2.11753 17.2874 2.83012 18 3.70577 18H14.294C15.1697 18 15.8822 17.2874 15.8822 16.4118V5.29412H14.8234V16.4118ZM6.35284 14.8235H7.41166V6.35294H6.35284V14.8235ZM10.5881 14.8235H11.6469V6.35294H10.5881V14.8235ZM12.5682 3.17647L11.4099 0H6.51176L5.35553 3.17647H0V4.23529H18V3.17647H12.5682ZM6.48105 3.17647L7.25293 1.05882H10.6698L11.4416 3.17647H6.48105Z\" fill=\"currentColor\"/></svg>";
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Copyright (C) 2023 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ export const thumbnailViewIcon = '<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_1209_167295)"><path d="M24 17H0V0H24V17ZM1.10983 1.10569V15.8943H22.8902V1.10569H1.10983Z" fill="currentColor"/><path d="M21.7804 14.7887H2.21973V2.21143H21.7804V14.7887ZM9.84978 11.4716L14.5665 8.63826L9.84978 5.80492V11.4716Z" fill="currentColor"/></g><defs><clipPath id="clip0_1209_167295"><rect width="24" height="17" fill="white"/></clipPath></defs></svg>';
20
+ export const learnViewIcon = '<svg width="31" height="22" viewBox="0 0 31 22" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_93_30328)"><path d="M31 22H0V0H31V22ZM1.43353 1.43089V20.5691H29.5665V1.43089H1.43353Z" fill="currentColor"/><path d="M27.9537 3.04102H19.7109V18.9597H27.9537V3.04102Z" fill="currentColor"/><path d="M18.2781 18.9597H3.04688V3.04102H18.2781V18.9597ZM8.02537 14.7989L14.3165 11.0907L8.02537 7.38244V14.7989Z" fill="currentColor"/></g><defs><clipPath id="clip0_93_30328"><rect width="31" height="22" fill="white"/></clipPath></defs></svg>';
21
+ export const collabViewIcon = '<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_1209_167302)"><path d="M24 17H0V0H24V17ZM1.10983 1.10569V15.8943H22.8902V1.10569H1.10983Z" fill="currentColor"/><path d="M21.6416 2.34961H17.3411V8.15449H21.6416V2.34961Z" fill="currentColor"/><path d="M16.2312 8.15449H2.3584V2.34961H16.2312V8.15449ZM8.60117 7.18701L11.7919 5.39026L8.60117 3.59351V7.18701Z" fill="currentColor"/><path d="M21.6415 12.5771H4.99414V13.4064H21.6415V12.5771Z" fill="currentColor"/><path d="M21.6415 13.8213H4.99414V14.6506H21.6415V13.8213Z" fill="currentColor"/><path d="M3.39886 14.6503C3.97349 14.6503 4.43932 14.1862 4.43932 13.6137C4.43932 13.0412 3.97349 12.5771 3.39886 12.5771C2.82423 12.5771 2.3584 13.0412 2.3584 13.6137C2.3584 14.1862 2.82423 14.6503 3.39886 14.6503Z" fill="currentColor"/><path d="M21.6415 9.39844H4.99414V10.2277H21.6415V9.39844Z" fill="currentColor"/><path d="M21.6415 10.6421H4.99414V11.4714H21.6415V10.6421Z" fill="currentColor"/><path d="M3.39886 11.4716C3.97349 11.4716 4.43932 11.0075 4.43932 10.435C4.43932 9.86253 3.97349 9.39844 3.39886 9.39844C2.82423 9.39844 2.3584 9.86253 2.3584 10.435C2.3584 11.0075 2.82423 11.4716 3.39886 11.4716Z" fill="currentColor"/></g><defs><clipPath id="clip0_1209_167302"><rect width="24" height="17" fill="white"/></clipPath></defs></svg>';
22
+ export const optionsIcon = '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_1217_47603)"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.46326 3.85039H0V2.65039H2.46326C2.7388 1.36449 3.88181 0.400391 5.24999 0.400391C6.61818 0.400391 7.76119 1.36449 8.03673 2.65039H18V3.85039H8.03673C7.76119 5.13629 6.61818 6.10039 5.24999 6.10039C3.88181 6.10039 2.7388 5.13629 2.46326 3.85039ZM3.59999 3.25039C3.59999 2.33912 4.33872 1.60039 5.24999 1.60039C6.16126 1.60039 6.89999 2.33912 6.89999 3.25039C6.89999 4.16166 6.16126 4.90039 5.24999 4.90039C4.33872 4.90039 3.59999 4.16166 3.59999 3.25039Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M0 8.40039H9.46326C9.7388 7.11449 10.8818 6.15039 12.25 6.15039C13.6182 6.15039 14.7612 7.11449 15.0367 8.40039H18V9.60039H15.0367C14.7612 10.8863 13.6182 11.8504 12.25 11.8504C10.8818 11.8504 9.7388 10.8863 9.46326 9.60039H0V8.40039ZM12.25 7.35039C11.3387 7.35039 10.6 8.08912 10.6 9.00039C10.6 9.91166 11.3387 10.6504 12.25 10.6504C13.1613 10.6504 13.9 9.91166 13.9 9.00039C13.9 8.08912 13.1613 7.35039 12.25 7.35039Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M5.24999 17.6004C6.61818 17.6004 7.76119 16.6363 8.03673 15.3504H18V14.1504H8.03673C7.76119 12.8645 6.61818 11.9004 5.24999 11.9004C3.88181 11.9004 2.7388 12.8645 2.46326 14.1504H0V15.3504H2.46326C2.7388 16.6363 3.88181 17.6004 5.24999 17.6004ZM5.24999 13.1004C4.33872 13.1004 3.59999 13.8391 3.59999 14.7504C3.59999 15.6617 4.33872 16.4004 5.24999 16.4004C6.16126 16.4004 6.89999 15.6617 6.89999 14.7504C6.89999 13.8391 6.16126 13.1004 5.24999 13.1004Z" fill="currentColor"/></g><defs><clipPath id="clip0_1217_47603"><rect width="18" height="18" fill="white"/></clipPath></defs></svg>';
23
+ export const removeIcon = '<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.8234 16.4118C14.8234 16.7029 14.5852 16.9412 14.294 16.9412H3.70577C3.41459 16.9412 3.17636 16.7029 3.17636 16.4118V5.29412H2.11753V16.4118C2.11753 17.2874 2.83012 18 3.70577 18H14.294C15.1697 18 15.8822 17.2874 15.8822 16.4118V5.29412H14.8234V16.4118ZM6.35284 14.8235H7.41166V6.35294H6.35284V14.8235ZM10.5881 14.8235H11.6469V6.35294H10.5881V14.8235ZM12.5682 3.17647L11.4099 0H6.51176L5.35553 3.17647H0V4.23529H18V3.17647H12.5682ZM6.48105 3.17647L7.25293 1.05882H10.6698L11.4416 3.17647H6.48105Z" fill="currentColor"/></svg>';
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Copyright (C) 2024 - present Instructure, Inc.
3
+ *
4
+ * This file is part of Canvas.
5
+ *
6
+ * Canvas is free software: you can redistribute it and/or modify it under
7
+ * the terms of the GNU Affero General Public License as published by the Free
8
+ * Software Foundation, version 3 of the License.
9
+ *
10
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ * details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License along
16
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ import tinymce from 'tinymce';
20
+ import formatMessage from '../../../format-message';
21
+ import { debounce } from '@instructure/debounce';
22
+ const clickCallbackPromise = import('../instructure_wordcount/clickCallback');
23
+ const TOOLTIP_MESSAGE = formatMessage('View word and character counts');
24
+ const UPDATE_DEBOUNCE_MS = 100;
25
+ function formatWordCount(count) {
26
+ return formatMessage(`{count, plural,
27
+ =0 {0 words}
28
+ one {1 word}
29
+ other {# words}
30
+ }`, {
31
+ count
32
+ });
33
+ }
34
+ tinymce.PluginManager.add('instructure_wordcount_header', function (ed) {
35
+ function updateWordCountDisplay() {
36
+ const count = ed.plugins.wordcount.body.getWordCount();
37
+ const button = ed.getContainer()?.querySelector(`[title*="${TOOLTIP_MESSAGE}"]`);
38
+ if (!button) {
39
+ return;
40
+ }
41
+ const textSpan = button.querySelector('.tox-tbtn__select-label');
42
+ if (textSpan) {
43
+ textSpan.textContent = formatWordCount(count);
44
+ }
45
+ const tooltip = `${TOOLTIP_MESSAGE} - ${formatWordCount(count)}`;
46
+ button.setAttribute('title', tooltip);
47
+ button.setAttribute('aria-label', tooltip);
48
+ }
49
+ ed.addCommand('instructureWordCountHeader', () => {
50
+ clickCallbackPromise.then(module => module.default(ed, document, {
51
+ skipEditorFocus: false
52
+ }));
53
+ });
54
+ ed.ui.registry.addButton('instructure_wordcount_header', {
55
+ text: formatWordCount(0),
56
+ tooltip: TOOLTIP_MESSAGE,
57
+ onAction: () => ed.execCommand('instructureWordCountHeader')
58
+ });
59
+ ed.on('PostRender', () => {
60
+ updateWordCountDisplay();
61
+ });
62
+ const debouncedUpdate = debounce(updateWordCountDisplay, UPDATE_DEBOUNCE_MS, {
63
+ trailing: true
64
+ });
65
+ ed.on('NodeChange', debouncedUpdate);
66
+ ed.on('KeyUp', debouncedUpdate);
67
+ ed.on('SetContent', debouncedUpdate);
68
+ ed.on('Change', debouncedUpdate);
69
+ ed.on('Undo', debouncedUpdate);
70
+ ed.on('Redo', debouncedUpdate);
71
+ ed.on('Paste', debouncedUpdate);
72
+ ed.on('init', () => {
73
+ updateWordCountDisplay();
74
+ });
75
+ });
@@ -26,7 +26,7 @@ export function asLink($element: any, editor: any): {
26
26
  export function asVideoElement($element: any): {
27
27
  $element: any;
28
28
  type: string;
29
- id: any;
29
+ id: string | null;
30
30
  titleText: any;
31
31
  appliedHeight: any;
32
32
  appliedWidth: any;
@@ -62,7 +62,6 @@ export function isFileLink($element: any, editor: any): boolean;
62
62
  export function isImageEmbed($element: any): boolean;
63
63
  export function isVideoElement($element: any): boolean;
64
64
  export function isAudioElement($element: any): boolean;
65
- export function findMediaPlayerIframe(elem: any): any;
66
65
  export const LINK_TYPE: "link";
67
66
  export const FILE_LINK_TYPE: "file-link";
68
67
  export const IMAGE_EMBED_TYPE: "image-embed";
@@ -21,6 +21,7 @@ import { isOnlyTextSelected } from '../../contentInsertionUtils';
21
21
  import formatMessage from '../../../format-message';
22
22
  import { isStudioEmbeddedMedia } from './StudioLtiSupportUtils';
23
23
  import { parseUrlPath } from '../../../util/url-util';
24
+ import { findMediaPlayerIframe } from './iframeUtils';
24
25
  const FILE_DOWNLOAD_PATH_REGEX = /^\/(courses\/\d+\/)?files\/\d+\/download$/;
25
26
  export const LINK_TYPE = 'link';
26
27
  export const FILE_LINK_TYPE = 'file-link';
@@ -208,22 +209,4 @@ export function isVideoElement($element) {
208
209
  }
209
210
  export function isAudioElement($element) {
210
211
  return isMediaElement($element, 'audio');
211
- }
212
- export function findMediaPlayerIframe(elem) {
213
- if (!elem) return null;
214
- if (elem.tagName === 'IFRAME') {
215
- // we have the iframe
216
- return elem;
217
- }
218
- if (elem.firstElementChild?.tagName === 'IFRAME') {
219
- // we have the shim tinymce puts around the iframe
220
- return elem.firstElementChild;
221
- }
222
- if (elem.classList.contains('mce-shim')) {
223
- // tinymce puts a <span class='mce-shin'> after the iframe (since v5, I think)
224
- if (elem.previousSibling?.tagName === 'IFRAME') {
225
- return elem.previousSibling;
226
- }
227
- }
228
- return null;
229
212
  }
@@ -1,4 +1,4 @@
1
- import { EditorEvent, Events } from 'tinymce';
1
+ import { Editor, EditorEvent, Events } from 'tinymce';
2
2
  /**
3
3
  * Interface for content item's 'custom' field, specifically for what is expected to come from Studio
4
4
  *
@@ -35,3 +35,11 @@ export declare function parseStudioOptions(element: Element | null): ParsedStudi
35
35
  * underlying iframe has a `data-studio-resizable='false'`
36
36
  */
37
37
  export declare function handleBeforeObjectSelected(e: EditorEvent<Events.ObjectSelectedEvent>): void;
38
+ export declare function findStudioLtiIframeFromSelection(selectedNode: Node): HTMLIFrameElement | null;
39
+ export declare const notifyStudioEmbedTypeChange: (editor: Editor, embedType: "thumbnail_embed" | "learn_embed" | "collaboration_embed") => void;
40
+ export type EmbedType = 'thumbnail_embed' | 'learn_embed' | 'collaboration_embed';
41
+ export declare const updateStudioIframeDimensions: (editor: Editor, width: number, height: number, embedType: EmbedType) => void;
42
+ type ValidStudioEmbedType = 'thumbnail_embed' | 'learn_embed' | 'collaboration_embed';
43
+ export declare const isValidEmbedType: (embedType: any) => embedType is ValidStudioEmbedType;
44
+ export declare const isValidDimension: (value: any) => value is number;
45
+ export {};