@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.
- package/CHANGELOG.md +27 -0
- package/es/enhance-user-content/doc_previews.js +1 -14
- package/es/index.d.ts +1 -0
- package/es/index.js +2 -1
- package/es/rce/AlertMessageArea.d.ts +2 -2
- package/es/rce/AlertMessageArea.js +4 -6
- package/es/rce/RCEGlobals.d.ts +2 -0
- package/es/rce/RCEGlobals.js +1 -0
- package/es/rce/RCEVariants.js +3 -3
- package/es/rce/RCEWrapper.d.ts +14 -12
- package/es/rce/RCEWrapper.js +206 -199
- package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.d.ts +2 -0
- package/es/rce/plugins/instructure_keyboard_shortcuts_header/clickCallback.js +45 -0
- package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.d.ts +1 -0
- package/es/rce/plugins/instructure_keyboard_shortcuts_header/plugin.js +43 -0
- package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.d.ts +1 -1
- package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolSelectionDialog/ExternalToolSelectionDialog.js +25 -25
- package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.d.ts +1 -1
- package/es/rce/plugins/instructure_record/AudioOptionsTray/TrayController.js +2 -1
- package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.d.ts +1 -1
- package/es/rce/plugins/instructure_record/VideoOptionsTray/TrayController.js +2 -1
- package/es/rce/plugins/instructure_record/mediaTranslations.js +1 -1
- package/es/rce/plugins/instructure_studio_media_options/plugin.js +109 -14
- package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.d.ts +5 -0
- package/es/rce/plugins/instructure_studio_media_options/studioToolbarIcons.js +23 -0
- package/es/rce/plugins/instructure_wordcount_header/plugin.d.ts +1 -0
- package/es/rce/plugins/instructure_wordcount_header/plugin.js +75 -0
- package/es/rce/plugins/shared/ContentSelection.d.ts +1 -2
- package/es/rce/plugins/shared/ContentSelection.js +1 -18
- package/es/rce/plugins/shared/StudioLtiSupportUtils.d.ts +9 -1
- package/es/rce/plugins/shared/StudioLtiSupportUtils.js +94 -1
- package/es/rce/plugins/shared/Upload/ComputerPanel.js +1 -1
- package/es/rce/plugins/shared/Upload/UploadFileModal.js +37 -4
- package/es/rce/plugins/shared/Upload/VideoUrlPanel.d.ts +15 -0
- package/es/rce/plugins/shared/Upload/VideoUrlPanel.js +51 -0
- package/es/rce/plugins/shared/Upload/videoValidationUtils.d.ts +7 -0
- package/es/rce/plugins/shared/Upload/videoValidationUtils.js +58 -0
- package/es/rce/plugins/shared/iframeUtils.d.ts +1 -0
- package/es/rce/plugins/shared/iframeUtils.js +37 -0
- package/es/rce/tinyRCE.js +2 -0
- package/es/sidebar/actions/upload.d.ts +1 -0
- package/es/sidebar/actions/upload.js +56 -0
- package/es/sidebar/containers/sidebarHandlers.d.ts +1 -0
- package/es/sidebar/containers/sidebarHandlers.js +2 -1
- package/es/translations/locales/ar.js +15 -6
- package/es/translations/locales/ca.js +15 -6
- package/es/translations/locales/cy.js +15 -6
- package/es/translations/locales/da-x-k12.js +15 -6
- package/es/translations/locales/da.js +15 -6
- package/es/translations/locales/de.js +15 -6
- package/es/translations/locales/el.js +6 -0
- package/es/translations/locales/en-AU-x-unimelb.js +15 -6
- package/es/translations/locales/en-GB-x-ukhe.js +15 -6
- package/es/translations/locales/en.js +30 -6
- package/es/translations/locales/en_AU.js +15 -6
- package/es/translations/locales/en_CA.js +15 -6
- package/es/translations/locales/en_CY.js +15 -6
- package/es/translations/locales/en_GB.js +15 -6
- package/es/translations/locales/es.js +15 -6
- package/es/translations/locales/es_ES.js +15 -6
- package/es/translations/locales/fa_IR.js +6 -3
- package/es/translations/locales/fi.js +15 -6
- package/es/translations/locales/fr.js +15 -6
- package/es/translations/locales/fr_CA.js +19 -10
- package/es/translations/locales/ga.js +15 -6
- package/es/translations/locales/he.js +6 -0
- package/es/translations/locales/hi.js +15 -6
- package/es/translations/locales/ht.js +15 -6
- package/es/translations/locales/hu.js +6 -6
- package/es/translations/locales/hy.js +6 -0
- package/es/translations/locales/id.js +15 -6
- package/es/translations/locales/is.js +21 -6
- package/es/translations/locales/it.js +15 -6
- package/es/translations/locales/ja.js +15 -6
- package/es/translations/locales/ko.js +6 -0
- package/es/translations/locales/mi.js +15 -6
- package/es/translations/locales/ms.js +15 -6
- package/es/translations/locales/nb-x-k12.js +15 -6
- package/es/translations/locales/nb.js +15 -6
- package/es/translations/locales/nl.js +15 -6
- package/es/translations/locales/nn.js +6 -6
- package/es/translations/locales/pl.js +15 -6
- package/es/translations/locales/pt.js +15 -6
- package/es/translations/locales/pt_BR.js +15 -6
- package/es/translations/locales/ru.js +15 -6
- package/es/translations/locales/sl.js +15 -6
- package/es/translations/locales/sv-x-k12.js +15 -6
- package/es/translations/locales/sv.js +15 -6
- package/es/translations/locales/th.js +15 -6
- package/es/translations/locales/tr.js +6 -3
- package/es/translations/locales/uk_UA.js +6 -3
- package/es/translations/locales/vi.js +15 -6
- package/es/translations/locales/zh-Hans.js +15 -6
- package/es/translations/locales/zh-Hant.js +15 -6
- package/es/translations/locales/zh.js +15 -6
- package/es/translations/locales/zh_HK.js +15 -6
- package/package.json +53 -53
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import PropTypes, { bool, shape } from 'prop-types';
|
|
20
|
+
import { findMediaPlayerIframe } from './iframeUtils';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Interface for content item's 'custom' field, specifically for what is expected to come from Studio
|
|
@@ -71,4 +72,96 @@ export function handleBeforeObjectSelected(e) {
|
|
|
71
72
|
if (targetElement.getAttribute('data-mce-p-data-studio-resizable') === 'false') {
|
|
72
73
|
targetElement.setAttribute('data-mce-resize', 'false');
|
|
73
74
|
}
|
|
74
|
-
}
|
|
75
|
+
}
|
|
76
|
+
export function findStudioLtiIframeFromSelection(selectedNode) {
|
|
77
|
+
let outerIframe = null;
|
|
78
|
+
|
|
79
|
+
// First, find the outer iframe
|
|
80
|
+
if (selectedNode.nodeName === 'IFRAME') {
|
|
81
|
+
outerIframe = selectedNode;
|
|
82
|
+
} else if (selectedNode.nodeType === Node.ELEMENT_NODE) {
|
|
83
|
+
// Look for iframe inside the selected element (the span)
|
|
84
|
+
outerIframe = selectedNode.querySelector('iframe');
|
|
85
|
+
}
|
|
86
|
+
if (!outerIframe) {
|
|
87
|
+
console.error('No outer iframe found');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Now try to access the content document of the outer iframe
|
|
92
|
+
try {
|
|
93
|
+
const outerIframeDoc = outerIframe.contentDocument || outerIframe.contentWindow?.document;
|
|
94
|
+
if (!outerIframeDoc) {
|
|
95
|
+
return outerIframe; // Return outer iframe as fallback
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Search for nested iframe with data-lti-launch attribute
|
|
99
|
+
const nestedIframe = outerIframeDoc.querySelector('iframe[data-lti-launch="true"]');
|
|
100
|
+
if (nestedIframe) {
|
|
101
|
+
return nestedIframe;
|
|
102
|
+
} else {
|
|
103
|
+
// Try to find any iframe inside
|
|
104
|
+
const anyNestedIframe = outerIframeDoc.querySelector('iframe');
|
|
105
|
+
if (anyNestedIframe) {
|
|
106
|
+
return anyNestedIframe;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('>> Cannot access outer iframe content (cross-origin):', error);
|
|
111
|
+
// Return the outer iframe as fallback since we can't access its contents
|
|
112
|
+
return outerIframe;
|
|
113
|
+
}
|
|
114
|
+
return outerIframe;
|
|
115
|
+
}
|
|
116
|
+
export const notifyStudioEmbedTypeChange = (editor, embedType) => {
|
|
117
|
+
const studioIframe = findStudioLtiIframeFromSelection(editor.selection.getNode());
|
|
118
|
+
if (studioIframe && studioIframe.contentWindow) {
|
|
119
|
+
studioIframe.contentWindow.postMessage({
|
|
120
|
+
subject: 'studio.embedTypeChanged',
|
|
121
|
+
embedType: embedType,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
}, '*');
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
export const updateStudioIframeDimensions = (editor, width, height, embedType) => {
|
|
127
|
+
const selectedNode = editor.selection.getNode();
|
|
128
|
+
const videoContainer = findMediaPlayerIframe(selectedNode);
|
|
129
|
+
if (videoContainer?.tagName !== 'IFRAME') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const tinymceIframeShim = videoContainer.parentElement;
|
|
133
|
+
if (!tinymceIframeShim) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
editor.dom.setStyles(tinymceIframeShim, {
|
|
137
|
+
width: `${width}px`,
|
|
138
|
+
height: `${height}px`
|
|
139
|
+
});
|
|
140
|
+
editor.dom.setStyles(videoContainer, {
|
|
141
|
+
width: `${width}px`,
|
|
142
|
+
height: `${height}px`
|
|
143
|
+
});
|
|
144
|
+
const href = editor.dom.getAttrib(tinymceIframeShim, 'data-mce-p-src');
|
|
145
|
+
if (href && embedType) {
|
|
146
|
+
if (embedType) {
|
|
147
|
+
// Replace thumbnail_embed, learn_embed, or collaboration_embed with the new embed type
|
|
148
|
+
const updatedHref = href.replace(/(thumbnail_embed|learn_embed|collaboration_embed)/g, embedType);
|
|
149
|
+
|
|
150
|
+
// updating only mce-p-src as in we only want to update the real src whenever we step out of the editor or save it
|
|
151
|
+
editor.dom.setAttrib(tinymceIframeShim, 'data-mce-p-src', updatedHref);
|
|
152
|
+
editor.nodeChanged();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
editor.fire('ObjectResized', {
|
|
156
|
+
// @ts-expect-error - needed for aligning tooltip with new iframe size
|
|
157
|
+
target: videoContainer,
|
|
158
|
+
width,
|
|
159
|
+
height
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
export const isValidEmbedType = embedType => {
|
|
163
|
+
return typeof embedType === 'string' && ['thumbnail_embed', 'learn_embed', 'collaboration_embed'].includes(embedType);
|
|
164
|
+
};
|
|
165
|
+
export const isValidDimension = value => {
|
|
166
|
+
return typeof value === 'number' && !isNaN(value) && isFinite(value) && value > 0;
|
|
167
|
+
};
|
|
@@ -238,7 +238,7 @@ export default function ComputerPanel({
|
|
|
238
238
|
});
|
|
239
239
|
},
|
|
240
240
|
renderIcon: IconTrashLine,
|
|
241
|
-
screenReaderLabel: formatMessage('
|
|
241
|
+
screenReaderLabel: formatMessage('Remove {filename}', {
|
|
242
242
|
filename
|
|
243
243
|
})
|
|
244
244
|
})), /*#__PURE__*/React.createElement(Flex.Item, {
|
|
@@ -31,9 +31,11 @@ import ImageOptionsForm from '../ImageOptionsForm';
|
|
|
31
31
|
import UsageRightsSelectBox from './UsageRightsSelectBox';
|
|
32
32
|
import { View } from '@instructure/ui-view';
|
|
33
33
|
import { UploadCanvasPanelIds, CanvasPanelTitles } from '../canvasContentUtils';
|
|
34
|
+
import { validateVideoUrl } from './videoValidationUtils';
|
|
34
35
|
const CanvasContentPanel = /*#__PURE__*/React.lazy(() => import('./CanvasContentPanel'));
|
|
35
36
|
const ComputerPanel = /*#__PURE__*/React.lazy(() => import('./ComputerPanel'));
|
|
36
37
|
const UrlPanel = /*#__PURE__*/React.lazy(() => import('./UrlPanel'));
|
|
38
|
+
const VideoUrlPanel = /*#__PURE__*/React.lazy(() => import('./VideoUrlPanel'));
|
|
37
39
|
function shouldBeDisabled({
|
|
38
40
|
fileUrl,
|
|
39
41
|
theFile,
|
|
@@ -46,6 +48,7 @@ function shouldBeDisabled({
|
|
|
46
48
|
case 'COMPUTER':
|
|
47
49
|
return !theFile || theFile.error;
|
|
48
50
|
case 'URL':
|
|
51
|
+
case 'VIDEO_URL':
|
|
49
52
|
return !fileUrl;
|
|
50
53
|
default:
|
|
51
54
|
if (UploadCanvasPanelIds.includes(selectedPanel)) return !fileUrl;
|
|
@@ -133,8 +136,17 @@ const UploadFileModal = /*#__PURE__*/React.forwardRef(({
|
|
|
133
136
|
if (submitDisabled || uploading) {
|
|
134
137
|
return false;
|
|
135
138
|
}
|
|
139
|
+
let finalFileUrl = fileUrl;
|
|
140
|
+
if (selectedPanel === 'VIDEO_URL' && finalFileUrl) {
|
|
141
|
+
const validation = validateVideoUrl(finalFileUrl);
|
|
142
|
+
if (!validation.isValid) {
|
|
143
|
+
setError('Invalid video URL');
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
finalFileUrl = validation.embedUrl;
|
|
147
|
+
}
|
|
136
148
|
onSubmit(editor, accept, selectedPanel, {
|
|
137
|
-
fileUrl,
|
|
149
|
+
fileUrl: finalFileUrl,
|
|
138
150
|
theFile,
|
|
139
151
|
imageOptions: {
|
|
140
152
|
altText,
|
|
@@ -155,7 +167,7 @@ const UploadFileModal = /*#__PURE__*/React.forwardRef(({
|
|
|
155
167
|
}), /*#__PURE__*/React.createElement(Heading, null, label)), /*#__PURE__*/React.createElement(Modal.Body, {
|
|
156
168
|
ref: ref
|
|
157
169
|
}, /*#__PURE__*/React.createElement(Tabs, {
|
|
158
|
-
onRequestTabChange: (
|
|
170
|
+
onRequestTabChange: (_event, {
|
|
159
171
|
index
|
|
160
172
|
}) => handleRequestTabChange(index)
|
|
161
173
|
}, panels.map(panel => {
|
|
@@ -197,7 +209,28 @@ const UploadFileModal = /*#__PURE__*/React.forwardRef(({
|
|
|
197
209
|
})
|
|
198
210
|
}, /*#__PURE__*/React.createElement(UrlPanel, {
|
|
199
211
|
fileUrl: fileUrl,
|
|
200
|
-
setFileUrl: setFileUrl
|
|
212
|
+
setFileUrl: setFileUrl,
|
|
213
|
+
urlHasError: !!error
|
|
214
|
+
})));
|
|
215
|
+
case 'VIDEO_URL':
|
|
216
|
+
return /*#__PURE__*/React.createElement(Tabs.Panel, {
|
|
217
|
+
key: panel,
|
|
218
|
+
renderTitle: function () {
|
|
219
|
+
return formatMessage('Video URL');
|
|
220
|
+
},
|
|
221
|
+
isSelected: selectedPanel === 'VIDEO_URL'
|
|
222
|
+
}, /*#__PURE__*/React.createElement(Suspense, {
|
|
223
|
+
fallback: /*#__PURE__*/React.createElement(Spinner, {
|
|
224
|
+
renderTitle: formatMessage('Loading'),
|
|
225
|
+
size: "large"
|
|
226
|
+
})
|
|
227
|
+
}, /*#__PURE__*/React.createElement(VideoUrlPanel, {
|
|
228
|
+
fileUrl: fileUrl,
|
|
229
|
+
setFileUrl: url => {
|
|
230
|
+
setError(null);
|
|
231
|
+
setFileUrl(url);
|
|
232
|
+
},
|
|
233
|
+
urlHasError: !!error
|
|
201
234
|
})));
|
|
202
235
|
default:
|
|
203
236
|
if (UploadCanvasPanelIds.includes(panel)) {
|
|
@@ -274,7 +307,7 @@ UploadFileModal.propTypes = {
|
|
|
274
307
|
canvasOrigin: string,
|
|
275
308
|
onSubmit: func,
|
|
276
309
|
onDismiss: func.isRequired,
|
|
277
|
-
panels: arrayOf(oneOf(['COMPUTER', 'URL', ...UploadCanvasPanelIds])),
|
|
310
|
+
panels: arrayOf(oneOf(['COMPUTER', 'URL', 'VIDEO_URL', ...UploadCanvasPanelIds])),
|
|
278
311
|
label: string.isRequired,
|
|
279
312
|
accept: oneOfType([arrayOf(string), string]),
|
|
280
313
|
modalBodyWidth: number,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare function VideoUrlPanel({ fileUrl, setFileUrl, urlHasError }: {
|
|
2
|
+
fileUrl: any;
|
|
3
|
+
setFileUrl: any;
|
|
4
|
+
urlHasError: any;
|
|
5
|
+
}): React.JSX.Element;
|
|
6
|
+
declare namespace VideoUrlPanel {
|
|
7
|
+
namespace propTypes {
|
|
8
|
+
export let fileUrl: import("prop-types").Validator<string>;
|
|
9
|
+
export let setFileUrl: import("prop-types").Validator<(...args: any[]) => any>;
|
|
10
|
+
export { bool as urlHasError };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export default VideoUrlPanel;
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { bool } from 'prop-types';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2025 - 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 { string, func, bool } from 'prop-types';
|
|
21
|
+
import { TextInput } from '@instructure/ui-text-input';
|
|
22
|
+
import formatMessage from '../../../../format-message';
|
|
23
|
+
export default function VideoUrlPanel({
|
|
24
|
+
fileUrl,
|
|
25
|
+
setFileUrl,
|
|
26
|
+
urlHasError
|
|
27
|
+
}) {
|
|
28
|
+
const handleChange = (_e, val) => {
|
|
29
|
+
setFileUrl(val);
|
|
30
|
+
};
|
|
31
|
+
const getErrorMessage = () => {
|
|
32
|
+
if (!urlHasError) return [];
|
|
33
|
+
return [{
|
|
34
|
+
text: formatMessage('Please enter a valid video URL from a supported platform.'),
|
|
35
|
+
type: 'newError'
|
|
36
|
+
}];
|
|
37
|
+
};
|
|
38
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, {
|
|
39
|
+
name: "video-url",
|
|
40
|
+
renderLabel: formatMessage('YouTube embed URL'),
|
|
41
|
+
type: "text",
|
|
42
|
+
value: fileUrl,
|
|
43
|
+
onChange: handleChange,
|
|
44
|
+
messages: getErrorMessage()
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
VideoUrlPanel.propTypes = {
|
|
48
|
+
fileUrl: string.isRequired,
|
|
49
|
+
setFileUrl: func.isRequired,
|
|
50
|
+
urlHasError: bool
|
|
51
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2020 - 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
|
+
function validateAndExtractYouTubeUrl(input) {
|
|
20
|
+
if (!input || typeof input !== 'string') {
|
|
21
|
+
return {
|
|
22
|
+
isValid: false,
|
|
23
|
+
embedUrl: null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const trimmedInput = input.trim();
|
|
27
|
+
const patterns = [/^(?:https?:\/\/)?(?:www\.|m\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)(?:[&?][^\s]*)?$/, /^(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([a-zA-Z0-9_-]+)(?:[?][^\s]*)?$/, /<iframe[^>]*src=["'](?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([a-zA-Z0-9_-]+)[^"']*["'][^>]*>/];
|
|
28
|
+
for (const pattern of patterns) {
|
|
29
|
+
const match = trimmedInput.match(pattern);
|
|
30
|
+
if (match && match[1]) {
|
|
31
|
+
const videoId = match[1];
|
|
32
|
+
return {
|
|
33
|
+
isValid: true,
|
|
34
|
+
embedUrl: `https://www.youtube.com/embed/${videoId}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
isValid: false,
|
|
40
|
+
embedUrl: null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function validateVideoUrl(input) {
|
|
44
|
+
if (!input || typeof input !== 'string') {
|
|
45
|
+
return {
|
|
46
|
+
isValid: false,
|
|
47
|
+
embedUrl: null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const youTubeRegexp = /(?:https?:\/\/)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be)/i;
|
|
51
|
+
if (youTubeRegexp.test(input)) {
|
|
52
|
+
return validateAndExtractYouTubeUrl(input);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
isValid: false,
|
|
56
|
+
embedUrl: null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findMediaPlayerIframe(elem: Element | null): Element | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2025 - 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 function findMediaPlayerIframe(elem) {
|
|
20
|
+
if (!elem) return null;
|
|
21
|
+
if (elem.tagName === 'IFRAME') {
|
|
22
|
+
// we have the iframe
|
|
23
|
+
return elem;
|
|
24
|
+
}
|
|
25
|
+
if (elem.firstElementChild?.tagName === 'IFRAME') {
|
|
26
|
+
// we have the shim tinymce puts around the iframe
|
|
27
|
+
return elem.firstElementChild;
|
|
28
|
+
}
|
|
29
|
+
if (elem.classList.contains('mce-shim')) {
|
|
30
|
+
// tinymce puts a <span class='mce-shin'> after the iframe (since v5, I think)
|
|
31
|
+
const prevSibling = elem.previousSibling;
|
|
32
|
+
if (prevSibling && 'tagName' in prevSibling && prevSibling.tagName === 'IFRAME') {
|
|
33
|
+
return prevSibling;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
package/es/rce/tinyRCE.js
CHANGED
|
@@ -55,6 +55,8 @@ import './plugins/instructure_html_view/plugin';
|
|
|
55
55
|
import './plugins/instructure_media_embed/plugin';
|
|
56
56
|
import './plugins/instructure_icon_maker/plugin';
|
|
57
57
|
import './plugins/instructure_wordcount/plugin';
|
|
58
|
+
import './plugins/instructure_wordcount_header/plugin';
|
|
59
|
+
import './plugins/instructure_keyboard_shortcuts_header/plugin';
|
|
58
60
|
import './plugins/instructure_paste/plugin';
|
|
59
61
|
import './plugins/instructure_fullscreen/plugin';
|
|
60
62
|
import './plugins/instructure_studio_media_options/plugin';
|
|
@@ -91,6 +91,7 @@ export function fetchFolders(bookmark: any): (dispatch: any, getState: any) => a
|
|
|
91
91
|
export function mediaUploadComplete(error: any, uploadData: any): (dispatch: any, _getState: any) => void;
|
|
92
92
|
export function createMediaServerSession(): (dispatch: any, getState: any) => any;
|
|
93
93
|
export function uploadToIconMakerFolder(svg: any, uploadSettings?: {}): (_dispatch: any, getState: any) => any;
|
|
94
|
+
export function uploadToMediaFolderWithoutEditor(fileMetaProps: any): (_: any, getState: any) => any;
|
|
94
95
|
export function uploadToMediaFolder(tabContext: any, fileMetaProps: any): (dispatch: any, getState: any) => any;
|
|
95
96
|
export function setUsageRights(source: any, fileMetaProps: any, results: any): any;
|
|
96
97
|
export function getFileUrlIfMissing(source: any, results: any): any;
|
|
@@ -330,6 +330,62 @@ export function uploadToIconMakerFolder(svg, uploadSettings = {}) {
|
|
|
330
330
|
});
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
|
+
export function uploadToMediaFolderWithoutEditor(fileMetaProps) {
|
|
334
|
+
return (_, getState) => {
|
|
335
|
+
const {
|
|
336
|
+
source,
|
|
337
|
+
jwt,
|
|
338
|
+
host,
|
|
339
|
+
contextId,
|
|
340
|
+
contextType
|
|
341
|
+
} = getState();
|
|
342
|
+
return source.fetchMediaFolder({
|
|
343
|
+
jwt,
|
|
344
|
+
host,
|
|
345
|
+
contextId,
|
|
346
|
+
contextType
|
|
347
|
+
}).then(async ({
|
|
348
|
+
folders
|
|
349
|
+
}) => {
|
|
350
|
+
fileMetaProps.parentFolderId = folders[0].id;
|
|
351
|
+
if (fileMetaProps.domObject) {
|
|
352
|
+
delete fileMetaProps.domObject.preview; // don't need this anymore
|
|
353
|
+
}
|
|
354
|
+
const getCategory = async fileProps => {
|
|
355
|
+
const categoryObject = await CategoryProcessor.process(fileProps.domObject);
|
|
356
|
+
return categoryObject?.category;
|
|
357
|
+
};
|
|
358
|
+
const category = await getCategory(fileMetaProps);
|
|
359
|
+
return source.preflightUpload(fileMetaProps, {
|
|
360
|
+
jwt,
|
|
361
|
+
host,
|
|
362
|
+
contextId,
|
|
363
|
+
contextType,
|
|
364
|
+
category
|
|
365
|
+
}).then(results => {
|
|
366
|
+
return source.uploadFRD(fileMetaProps.domObject, results);
|
|
367
|
+
}).then(results => {
|
|
368
|
+
return setUsageRights(source, fileMetaProps, results);
|
|
369
|
+
}).then(results => {
|
|
370
|
+
return getFileUrlIfMissing(source, results);
|
|
371
|
+
}).then(results => {
|
|
372
|
+
return fixupFileUrl(contextType, contextId, results, source.canvasOrigin);
|
|
373
|
+
}).then(results => {
|
|
374
|
+
return setAltText(fileMetaProps.altText, results);
|
|
375
|
+
}).then(results => {
|
|
376
|
+
if (fileMetaProps.isDecorativeImage) {
|
|
377
|
+
results.isDecorativeImage = fileMetaProps.isDecorativeImage;
|
|
378
|
+
}
|
|
379
|
+
if (fileMetaProps.displayAs) {
|
|
380
|
+
results.displayAs = fileMetaProps.displayAs;
|
|
381
|
+
}
|
|
382
|
+
return results;
|
|
383
|
+
});
|
|
384
|
+
}).catch(e => {
|
|
385
|
+
console.error('Upload to the media folder failed.', e);
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
}
|
|
333
389
|
export function uploadToMediaFolder(tabContext, fileMetaProps) {
|
|
334
390
|
return (dispatch, getState) => {
|
|
335
391
|
const editorComponent = bridge.activeEditor();
|
|
@@ -19,6 +19,7 @@ export default function propsFromDispatch(dispatch: any): {
|
|
|
19
19
|
toggleNewPageForm: () => any;
|
|
20
20
|
startIconMakerUpload: (fileMetaProps: any, uploadSettings: any) => any;
|
|
21
21
|
startMediaUpload: (tabContext: any, fileMetaProps: any) => any;
|
|
22
|
+
startMediaUploadInStandaloneMode: (fileMetaProps: any) => any;
|
|
22
23
|
createMediaServerSession: () => any;
|
|
23
24
|
mediaUploadComplete: (error: any, uploadData: any) => any;
|
|
24
25
|
fetchInitialDocs: () => any;
|
|
@@ -21,7 +21,7 @@ import { fetchInitialPage, fetchNextPage } from '../actions/data';
|
|
|
21
21
|
import { fetchInitialImages, fetchNextImages } from '../actions/images';
|
|
22
22
|
import { createMediaServerSession, fetchFolders, openOrCloseUploadForm,
|
|
23
23
|
// saveMediaRecording,
|
|
24
|
-
mediaUploadComplete, uploadPreflight, uploadToIconMakerFolder, uploadToMediaFolder } from '../actions/upload';
|
|
24
|
+
mediaUploadComplete, uploadPreflight, uploadToIconMakerFolder, uploadToMediaFolder, uploadToMediaFolderWithoutEditor } from '../actions/upload';
|
|
25
25
|
import { searchFlickr, openOrCloseFlickrForm } from '../actions/flickr';
|
|
26
26
|
import { toggle as toggleFolder } from '../actions/files';
|
|
27
27
|
import { openOrCloseNewPageForm } from '../actions/links';
|
|
@@ -53,6 +53,7 @@ export default function propsFromDispatch(dispatch) {
|
|
|
53
53
|
toggleNewPageForm: () => dispatch(openOrCloseNewPageForm()),
|
|
54
54
|
startIconMakerUpload: (fileMetaProps, uploadSettings) => dispatch(uploadToIconMakerFolder(fileMetaProps, uploadSettings)),
|
|
55
55
|
startMediaUpload: (tabContext, fileMetaProps) => dispatch(uploadToMediaFolder(tabContext, fileMetaProps)),
|
|
56
|
+
startMediaUploadInStandaloneMode: fileMetaProps => dispatch(uploadToMediaFolderWithoutEditor(fileMetaProps)),
|
|
56
57
|
createMediaServerSession: () => dispatch(createMediaServerSession()),
|
|
57
58
|
// saveMediaRecording: (file, editor, dismiss) => dispatch(saveMediaRecording(file, editor, dismiss)),
|
|
58
59
|
mediaUploadComplete: (error, uploadData) => dispatch(mediaUploadComplete(error, uploadData)),
|
|
@@ -301,12 +301,6 @@ const locale = {
|
|
|
301
301
|
"clear_image_3213fe62": {
|
|
302
302
|
"message": "مسح الصورة"
|
|
303
303
|
},
|
|
304
|
-
"clear_selected_file_82388e50": {
|
|
305
|
-
"message": "مسح الملف المحدد"
|
|
306
|
-
},
|
|
307
|
-
"clear_selected_file_filename_2fe8a58e": {
|
|
308
|
-
"message": "مسح الملف المحدد: { filename }"
|
|
309
|
-
},
|
|
310
304
|
"click_or_shift_click_for_the_html_editor_25d70bb4": {
|
|
311
305
|
"message": "انقر أو نقرة + shift للوصول إلى محرر html."
|
|
312
306
|
},
|
|
@@ -1711,6 +1705,9 @@ const locale = {
|
|
|
1711
1705
|
"please_enter_a_file_name_f159edc1": {
|
|
1712
1706
|
"message": "يرجى إدخال اسم الملف"
|
|
1713
1707
|
},
|
|
1708
|
+
"please_enter_a_valid_video_url_from_a_supported_pl_30dc0596": {
|
|
1709
|
+
"message": "يُرجى إدخال عنوان URL صالح للفيديو من منصة مدعومة."
|
|
1710
|
+
},
|
|
1714
1711
|
"please_select_a_file_of_a_supported_type_1fc578f2": {
|
|
1715
1712
|
"message": "يرجى تحديد ملف بنوع مدعوم"
|
|
1716
1713
|
},
|
|
@@ -1828,6 +1825,12 @@ const locale = {
|
|
|
1828
1825
|
"religion_icon_246e0be1": {
|
|
1829
1826
|
"message": "أيقونة الدين"
|
|
1830
1827
|
},
|
|
1828
|
+
"remove_f47dc62a": {
|
|
1829
|
+
"message": "إزالة"
|
|
1830
|
+
},
|
|
1831
|
+
"remove_filename_3ea029f6": {
|
|
1832
|
+
"message": "إزالة { filename }"
|
|
1833
|
+
},
|
|
1831
1834
|
"remove_heading_style_5fdc8855": {
|
|
1832
1835
|
"message": "إزالة نمط العنوان"
|
|
1833
1836
|
},
|
|
@@ -2542,6 +2545,9 @@ const locale = {
|
|
|
2542
2545
|
"video_player_for_title_ffd9fbc4": {
|
|
2543
2546
|
"message": "مشغل الفيديو لـ { title }"
|
|
2544
2547
|
},
|
|
2548
|
+
"video_url_889d3263": {
|
|
2549
|
+
"message": "عنوان URL الفيديو"
|
|
2550
|
+
},
|
|
2545
2551
|
"view_all_e13bf0a6": {
|
|
2546
2552
|
"message": "عرض الكل"
|
|
2547
2553
|
},
|
|
@@ -2656,6 +2662,9 @@ const locale = {
|
|
|
2656
2662
|
"your_webcam_may_already_be_in_use_6cd64c25": {
|
|
2657
2663
|
"message": "قد تكون كاميرا الويب الخاصة بك قيد الاستخدام بالفعل."
|
|
2658
2664
|
},
|
|
2665
|
+
"youtube_embed_url_5c1018d4": {
|
|
2666
|
+
"message": "عنوان URL المضمن لـ YouTube"
|
|
2667
|
+
},
|
|
2659
2668
|
"zeta_5ef24f0e": {
|
|
2660
2669
|
"message": "Zeta"
|
|
2661
2670
|
},
|
|
@@ -301,12 +301,6 @@ const locale = {
|
|
|
301
301
|
"clear_image_3213fe62": {
|
|
302
302
|
"message": "Esborra la imatge"
|
|
303
303
|
},
|
|
304
|
-
"clear_selected_file_82388e50": {
|
|
305
|
-
"message": "Esborra el fitxer seleccionat"
|
|
306
|
-
},
|
|
307
|
-
"clear_selected_file_filename_2fe8a58e": {
|
|
308
|
-
"message": "Esborra el fitxer seleccionat: { filename }"
|
|
309
|
-
},
|
|
310
304
|
"click_or_shift_click_for_the_html_editor_25d70bb4": {
|
|
311
305
|
"message": "Feu clic aquí o feu-hi clic mentre premeu la tecla Maj per obrir l’editor d’HTML."
|
|
312
306
|
},
|
|
@@ -1711,6 +1705,9 @@ const locale = {
|
|
|
1711
1705
|
"please_enter_a_file_name_f159edc1": {
|
|
1712
1706
|
"message": "Introduïu un nom de fitxer"
|
|
1713
1707
|
},
|
|
1708
|
+
"please_enter_a_valid_video_url_from_a_supported_pl_30dc0596": {
|
|
1709
|
+
"message": "Introduïu un URL de vídeo vàlid des d’una plataforma admesa."
|
|
1710
|
+
},
|
|
1714
1711
|
"please_select_a_file_of_a_supported_type_1fc578f2": {
|
|
1715
1712
|
"message": "Seleccioneu un fitxer d’un tipus admès"
|
|
1716
1713
|
},
|
|
@@ -1828,6 +1825,12 @@ const locale = {
|
|
|
1828
1825
|
"religion_icon_246e0be1": {
|
|
1829
1826
|
"message": "Icona de religió"
|
|
1830
1827
|
},
|
|
1828
|
+
"remove_f47dc62a": {
|
|
1829
|
+
"message": "Suprimeix-ho"
|
|
1830
|
+
},
|
|
1831
|
+
"remove_filename_3ea029f6": {
|
|
1832
|
+
"message": "Suprimiu { filename }"
|
|
1833
|
+
},
|
|
1831
1834
|
"remove_heading_style_5fdc8855": {
|
|
1832
1835
|
"message": "Suprimeix l''estil de la capçalera"
|
|
1833
1836
|
},
|
|
@@ -2542,6 +2545,9 @@ const locale = {
|
|
|
2542
2545
|
"video_player_for_title_ffd9fbc4": {
|
|
2543
2546
|
"message": "Reproductor de vídeo per a { title }"
|
|
2544
2547
|
},
|
|
2548
|
+
"video_url_889d3263": {
|
|
2549
|
+
"message": "URL de vídeo"
|
|
2550
|
+
},
|
|
2545
2551
|
"view_all_e13bf0a6": {
|
|
2546
2552
|
"message": "Mostra-ho tot"
|
|
2547
2553
|
},
|
|
@@ -2656,6 +2662,9 @@ const locale = {
|
|
|
2656
2662
|
"your_webcam_may_already_be_in_use_6cd64c25": {
|
|
2657
2663
|
"message": "És possible que la càmera web ja s''estigui utilitzant."
|
|
2658
2664
|
},
|
|
2665
|
+
"youtube_embed_url_5c1018d4": {
|
|
2666
|
+
"message": "URL integrat de YouTube"
|
|
2667
|
+
},
|
|
2659
2668
|
"zeta_5ef24f0e": {
|
|
2660
2669
|
"message": "Zeta"
|
|
2661
2670
|
},
|
|
@@ -301,12 +301,6 @@ const locale = {
|
|
|
301
301
|
"clear_image_3213fe62": {
|
|
302
302
|
"message": "Clirio’r ddelwedd"
|
|
303
303
|
},
|
|
304
|
-
"clear_selected_file_82388e50": {
|
|
305
|
-
"message": "Clirio''r ffeil dan sylw"
|
|
306
|
-
},
|
|
307
|
-
"clear_selected_file_filename_2fe8a58e": {
|
|
308
|
-
"message": "Clirio''r ffeil dan sylw: { filename }"
|
|
309
|
-
},
|
|
310
304
|
"click_or_shift_click_for_the_html_editor_25d70bb4": {
|
|
311
305
|
"message": "Cliciwch neu pwyswch shifft a chlicio ar gyfer y golygydd html."
|
|
312
306
|
},
|
|
@@ -1711,6 +1705,9 @@ const locale = {
|
|
|
1711
1705
|
"please_enter_a_file_name_f159edc1": {
|
|
1712
1706
|
"message": "Rhowch enw ffeil"
|
|
1713
1707
|
},
|
|
1708
|
+
"please_enter_a_valid_video_url_from_a_supported_pl_30dc0596": {
|
|
1709
|
+
"message": "Rhowch URL fideo dilys o blatfform a gefnogir."
|
|
1710
|
+
},
|
|
1714
1711
|
"please_select_a_file_of_a_supported_type_1fc578f2": {
|
|
1715
1712
|
"message": "Dewiswch ffeil o fath sy''n cael ei gefnogi"
|
|
1716
1713
|
},
|
|
@@ -1828,6 +1825,12 @@ const locale = {
|
|
|
1828
1825
|
"religion_icon_246e0be1": {
|
|
1829
1826
|
"message": "Eicon Crefydd"
|
|
1830
1827
|
},
|
|
1828
|
+
"remove_f47dc62a": {
|
|
1829
|
+
"message": "Tynnu"
|
|
1830
|
+
},
|
|
1831
|
+
"remove_filename_3ea029f6": {
|
|
1832
|
+
"message": "Tynnu { filename }"
|
|
1833
|
+
},
|
|
1831
1834
|
"remove_heading_style_5fdc8855": {
|
|
1832
1835
|
"message": "Tynnu arddull y pennawd"
|
|
1833
1836
|
},
|
|
@@ -2542,6 +2545,9 @@ const locale = {
|
|
|
2542
2545
|
"video_player_for_title_ffd9fbc4": {
|
|
2543
2546
|
"message": "Chwaraewr fideo ar gyfer { title }"
|
|
2544
2547
|
},
|
|
2548
|
+
"video_url_889d3263": {
|
|
2549
|
+
"message": "URL Fideo"
|
|
2550
|
+
},
|
|
2545
2551
|
"view_all_e13bf0a6": {
|
|
2546
2552
|
"message": "Gweld Pob Un"
|
|
2547
2553
|
},
|
|
@@ -2656,6 +2662,9 @@ const locale = {
|
|
|
2656
2662
|
"your_webcam_may_already_be_in_use_6cd64c25": {
|
|
2657
2663
|
"message": "Efallai bod eich gwe-gamera yn cael ei ddefnyddio’n barod."
|
|
2658
2664
|
},
|
|
2665
|
+
"youtube_embed_url_5c1018d4": {
|
|
2666
|
+
"message": "URL plannu YouTube"
|
|
2667
|
+
},
|
|
2659
2668
|
"zeta_5ef24f0e": {
|
|
2660
2669
|
"message": "Zeta"
|
|
2661
2670
|
},
|