@instructure/canvas-rce 5.12.2 → 5.13.1
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 +33 -1
- package/es/common/fileUrl.js +5 -1
- package/es/defaultTinymceConfig.js +2 -1
- package/es/enhance-user-content/enhance_user_content.js +30 -1
- package/es/getTranslations.js +5 -1
- package/es/rce/RCEWrapper.js +63 -22
- package/es/rce/RCEWrapperProps.js +1 -0
- package/es/rce/StatusBar.js +5 -4
- package/es/rce/editorLanguage.js +2 -0
- package/es/rce/plugins/instructure_image/ImageEmbedOptions.js +1 -1
- package/es/rce/plugins/instructure_rce_external_tools/ExternalToolsEnv.js +6 -0
- package/es/rce/plugins/instructure_rce_external_tools/RceToolWrapper.js +30 -1
- package/es/rce/plugins/instructure_rce_external_tools/components/ExternalToolDialog/ExternalToolDialog.js +14 -1
- package/es/rce/plugins/instructure_rce_external_tools/lti13-content-items/processEditorContentItems.js +9 -3
- package/es/rce/plugins/instructure_rce_external_tools/plugin.js +3 -2
- package/es/rce/plugins/instructure_search_and_replace/clickCallback.js +55 -0
- package/es/rce/plugins/instructure_search_and_replace/components/FindReplaceTray.js +360 -0
- package/es/rce/plugins/instructure_search_and_replace/components/FindReplaceTrayController.js +139 -0
- package/es/rce/plugins/instructure_search_and_replace/getSelectionContext.js +68 -0
- package/es/rce/plugins/instructure_search_and_replace/plugin.js +39 -0
- package/es/rce/plugins/instructure_search_and_replace/types.d.js +1 -0
- package/es/rce/plugins/shared/fileTypeUtils.js +3 -1
- package/es/rce/plugins/tinymce-a11y-checker/plugin.js +11 -1
- package/es/rce/plugins/tinymce-a11y-checker/utils/indicate.js +1 -1
- package/es/rce/tinyRCE.js +3 -1
- package/es/translations/locales/ar.js +54 -0
- package/es/translations/locales/ca.js +54 -0
- package/es/translations/locales/cy.js +54 -0
- package/es/translations/locales/da-x-k12.js +54 -0
- package/es/translations/locales/da.js +54 -0
- package/es/translations/locales/de.js +54 -0
- package/es/translations/locales/el.js +9 -0
- package/es/translations/locales/en-AU-x-unimelb.js +54 -0
- package/es/translations/locales/en-GB-x-ukhe.js +54 -0
- package/es/translations/locales/en.js +54 -0
- package/es/translations/locales/en_AU.js +54 -0
- package/es/translations/locales/en_CA.js +54 -0
- package/es/translations/locales/en_CY.js +54 -0
- package/es/translations/locales/en_GB.js +54 -0
- package/es/translations/locales/es.js +54 -0
- package/es/translations/locales/es_ES.js +54 -0
- package/es/translations/locales/fa_IR.js +9 -0
- package/es/translations/locales/fi.js +54 -0
- package/es/translations/locales/fr.js +54 -0
- package/es/translations/locales/fr_CA.js +54 -0
- package/es/translations/locales/ga.js +2427 -0
- package/es/translations/locales/he.js +10 -1
- package/es/translations/locales/ht.js +54 -0
- package/es/translations/locales/hu.js +15 -0
- package/es/translations/locales/hy.js +9 -0
- package/es/translations/locales/id.js +55 -0
- package/es/translations/locales/id_ID.js +1 -0
- package/es/translations/locales/is.js +54 -0
- package/es/translations/locales/it.js +54 -0
- package/es/translations/locales/ja.js +61 -7
- package/es/translations/locales/ko.js +9 -0
- package/es/translations/locales/mi.js +54 -0
- package/es/translations/locales/ms.js +54 -0
- package/es/translations/locales/nb-x-k12.js +54 -0
- package/es/translations/locales/nb.js +54 -0
- package/es/translations/locales/nl.js +54 -0
- package/es/translations/locales/nn.js +9 -0
- package/es/translations/locales/pl.js +54 -0
- package/es/translations/locales/pt.js +54 -0
- package/es/translations/locales/pt_BR.js +54 -0
- package/es/translations/locales/ru.js +54 -0
- package/es/translations/locales/sl.js +54 -0
- package/es/translations/locales/sv-x-k12.js +54 -0
- package/es/translations/locales/sv.js +54 -0
- package/es/translations/locales/th.js +54 -0
- package/es/translations/locales/tr.js +9 -0
- package/es/translations/locales/uk_UA.js +9 -0
- package/es/translations/locales/vi.js +54 -0
- package/es/translations/locales/zh-Hans.js +54 -0
- package/es/translations/locales/zh-Hant.js +54 -0
- package/es/translations/locales/zh.js +54 -0
- package/es/translations/locales/zh_HK.js +54 -0
- package/es/translations/tinymce/ga.js +423 -0
- package/es/translations/tinymce/id.js +423 -0
- package/es/translations/tinymce/ja.js +1 -1
- package/package.json +3 -3
- package/scripts/commitTranslations.sh +2 -2
- package/scripts/publish_to_npm.sh +1 -1
- package/bin/jira_tickets.sh +0 -14
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import _pt from "prop-types";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (C) 2019 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* This file is part of Canvas.
|
|
7
|
+
*
|
|
8
|
+
* Canvas is free software: you can redistribute it and/or modify it under
|
|
9
|
+
* the terms of the GNU Affero General Public License as published by the Free
|
|
10
|
+
* Software Foundation, version 3 of the License.
|
|
11
|
+
*
|
|
12
|
+
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
13
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
14
|
+
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
15
|
+
* details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License along
|
|
18
|
+
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
21
|
+
import { Button, CloseButton } from '@instructure/ui-buttons';
|
|
22
|
+
import { Flex } from '@instructure/ui-flex';
|
|
23
|
+
import { Heading } from '@instructure/ui-heading';
|
|
24
|
+
import { getTrayHeight } from '../../shared/trayUtils';
|
|
25
|
+
import { View } from '@instructure/ui-view';
|
|
26
|
+
import { instuiPopupMountNode } from '../../../../util/fullscreenHelpers';
|
|
27
|
+
import formatMessage from '../../../../format-message';
|
|
28
|
+
import { Tray } from '@instructure/ui-tray';
|
|
29
|
+
import { TextInput } from '@instructure/ui-text-input';
|
|
30
|
+
import { Text } from '@instructure/ui-text';
|
|
31
|
+
import { ScreenReaderContent } from '@instructure/ui-a11y-content';
|
|
32
|
+
import { Alert } from '@instructure/ui-alerts';
|
|
33
|
+
export default function FindReplaceTray(_ref) {
|
|
34
|
+
let {
|
|
35
|
+
onNext,
|
|
36
|
+
onPrevious,
|
|
37
|
+
onFind,
|
|
38
|
+
onRequestClose,
|
|
39
|
+
onReplace,
|
|
40
|
+
index,
|
|
41
|
+
max,
|
|
42
|
+
initialText = '',
|
|
43
|
+
selectionContext = ['', '']
|
|
44
|
+
} = _ref;
|
|
45
|
+
const [findText, setFindText] = useState(initialText);
|
|
46
|
+
const [replaceText, setReplaceText] = useState('');
|
|
47
|
+
const [hasOpened, setHasOpened] = useState(false);
|
|
48
|
+
const [showReplaceAlert, setShowReplaceAlert] = useState('');
|
|
49
|
+
const [alertFindText, setAlertFindText] = useState('');
|
|
50
|
+
const [alertReplaceText, setAlertReplaceText] = useState('');
|
|
51
|
+
const trayRef = useRef(null);
|
|
52
|
+
const liveRegionKey = useRef(0);
|
|
53
|
+
const srDupKey = useRef(0); // moves RCE when tray opens/closes, copied from CanvasContentTray
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
var _trayRef$current;
|
|
57
|
+
|
|
58
|
+
if (!hasOpened) return;
|
|
59
|
+
let c = document.querySelector('[role="main"]');
|
|
60
|
+
let target_w = 0;
|
|
61
|
+
if (!c) return;
|
|
62
|
+
const margin = window.getComputedStyle(c).direction === 'ltr' ? document.body.getBoundingClientRect().right - c.getBoundingClientRect().right : c.getBoundingClientRect().left;
|
|
63
|
+
target_w = c.offsetWidth - ((_trayRef$current = trayRef.current) === null || _trayRef$current === void 0 ? void 0 : _trayRef$current.offsetWidth) + margin;
|
|
64
|
+
|
|
65
|
+
if (target_w >= 320 && target_w < c.offsetWidth) {
|
|
66
|
+
c.style.boxSizing = 'border-box';
|
|
67
|
+
c.style.width = `${target_w}px`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
c = document.querySelector('[role="main"]');
|
|
72
|
+
if (!c) return;
|
|
73
|
+
c.style.width = '';
|
|
74
|
+
};
|
|
75
|
+
}, [hasOpened]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (initialText) {
|
|
78
|
+
onFind(initialText);
|
|
79
|
+
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
function usePrevious(value) {
|
|
84
|
+
const ref = useRef();
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
ref.current = value;
|
|
87
|
+
}, [value]);
|
|
88
|
+
return ref.current;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const prepend = selectionContext[0];
|
|
92
|
+
const append = selectionContext[1];
|
|
93
|
+
const srContextMsg = formatMessage('{prepend}{findText}{append}', {
|
|
94
|
+
prepend,
|
|
95
|
+
findText,
|
|
96
|
+
append
|
|
97
|
+
});
|
|
98
|
+
const previousSrMsg = usePrevious(srContextMsg);
|
|
99
|
+
const previousFindText = usePrevious(findText);
|
|
100
|
+
|
|
101
|
+
const handleTextChange = e => {
|
|
102
|
+
setFindText(e.target.value);
|
|
103
|
+
onFind(e.target.value);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleFindKeyDown = e => {
|
|
107
|
+
if (e.key !== 'Enter') return;
|
|
108
|
+
|
|
109
|
+
if (e.shiftKey) {
|
|
110
|
+
if (isButtonDisabled('previous')) return;
|
|
111
|
+
onPrevious();
|
|
112
|
+
} else {
|
|
113
|
+
// try to search when there are currently no results and user presses enter
|
|
114
|
+
if (index === 0 && max === 0 && findText) {
|
|
115
|
+
onFind(findText);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isButtonDisabled('next')) return;
|
|
120
|
+
onNext();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleReplaceKeyDown = e => {
|
|
125
|
+
if (e.key !== 'Enter') return;
|
|
126
|
+
if (isButtonDisabled('replace')) return;
|
|
127
|
+
const forward = !e.shiftKey;
|
|
128
|
+
replace(replaceText, forward, false);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const replace = (newText, forward, all) => {
|
|
132
|
+
onReplace(newText, forward, all);
|
|
133
|
+
|
|
134
|
+
if (all) {
|
|
135
|
+
setShowReplaceAlert('replaceAll');
|
|
136
|
+
} else {
|
|
137
|
+
setShowReplaceAlert('replace');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setAlertFindText(findText);
|
|
141
|
+
setAlertReplaceText(newText);
|
|
142
|
+
liveRegionKey.current++;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const alertText = showReplaceAlert === '' ? '' : showReplaceAlert === 'replace' ? formatMessage('Replaced {alertFindText} with {alertReplaceText}', {
|
|
146
|
+
alertFindText,
|
|
147
|
+
alertReplaceText
|
|
148
|
+
}) : formatMessage('Replaced all {alertFindText} with {alertReplaceText}', {
|
|
149
|
+
alertFindText,
|
|
150
|
+
alertReplaceText
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const renderReplaceAlert = () => {
|
|
154
|
+
if (!showReplaceAlert) {
|
|
155
|
+
liveRegionKey.current = 0;
|
|
156
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return /*#__PURE__*/React.createElement(Alert, {
|
|
160
|
+
variant: "success",
|
|
161
|
+
renderCloseButtonLabel: "Close Alert",
|
|
162
|
+
margin: "small",
|
|
163
|
+
transition: "fade",
|
|
164
|
+
onDismiss: () => setShowReplaceAlert(''),
|
|
165
|
+
timeout: 3000
|
|
166
|
+
}, alertText);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const renderScreenReaderAlert = () => {
|
|
170
|
+
return /*#__PURE__*/React.createElement(ScreenReaderContent, null, alertText);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const errMsg = formatMessage('No results found');
|
|
174
|
+
const messages = findText && max === 0 ? [{
|
|
175
|
+
text: errMsg,
|
|
176
|
+
type: 'error'
|
|
177
|
+
}] : [];
|
|
178
|
+
|
|
179
|
+
const resultText = () => {
|
|
180
|
+
const srErrMsg = messages.length === 0 ? '' : errMsg; // resolves issue where focus is lost when this (dis)appears
|
|
181
|
+
|
|
182
|
+
if (max === 0) {
|
|
183
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ScreenReaderContent, {
|
|
184
|
+
"aria-live": "polite"
|
|
185
|
+
}, srErrMsg), /*#__PURE__*/React.createElement(Text, null));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const msg = formatMessage('{index} of {max}', {
|
|
189
|
+
index,
|
|
190
|
+
max
|
|
191
|
+
});
|
|
192
|
+
const srResultMsg = formatMessage('Result {index} of {max}.', {
|
|
193
|
+
index,
|
|
194
|
+
max
|
|
195
|
+
}); // necessary to force screen reader to read the same message while typing
|
|
196
|
+
|
|
197
|
+
if (srContextMsg === previousSrMsg && previousFindText != findText) {
|
|
198
|
+
srDupKey.current++;
|
|
199
|
+
} else srDupKey.current = 0;
|
|
200
|
+
|
|
201
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
|
|
202
|
+
as: "span",
|
|
203
|
+
key: srDupKey.current,
|
|
204
|
+
"aria-live": "polite",
|
|
205
|
+
"aria-atomic": "true",
|
|
206
|
+
role: "alert"
|
|
207
|
+
}, /*#__PURE__*/React.createElement(ScreenReaderContent, null, srResultMsg, " ", srContextMsg)), /*#__PURE__*/React.createElement(Text, {
|
|
208
|
+
"aria-hidden": "true"
|
|
209
|
+
}, msg));
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const isButtonDisabled = button => {
|
|
213
|
+
switch (button) {
|
|
214
|
+
case 'next':
|
|
215
|
+
case 'previous':
|
|
216
|
+
return max < 2;
|
|
217
|
+
|
|
218
|
+
case 'replace':
|
|
219
|
+
return !replaceText || index === 0;
|
|
220
|
+
|
|
221
|
+
case 'replaceAll':
|
|
222
|
+
return !replaceText || max < 2 || index === 0;
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return /*#__PURE__*/React.createElement(Tray, {
|
|
230
|
+
"data-mce-component": true,
|
|
231
|
+
label: formatMessage('Find and Replace'),
|
|
232
|
+
mountNode: instuiPopupMountNode,
|
|
233
|
+
onDismiss: onRequestClose,
|
|
234
|
+
open: true,
|
|
235
|
+
placement: "end",
|
|
236
|
+
size: "regular",
|
|
237
|
+
shouldContainFocus: true,
|
|
238
|
+
shouldReturnFocus: true,
|
|
239
|
+
shouldCloseOnDocumentClick: true,
|
|
240
|
+
onOpen: () => setHasOpened(true)
|
|
241
|
+
}, /*#__PURE__*/React.createElement(View, {
|
|
242
|
+
as: "div",
|
|
243
|
+
margin: "none none medium none",
|
|
244
|
+
key: liveRegionKey.current
|
|
245
|
+
}, renderReplaceAlert(), /*#__PURE__*/React.createElement(View, {
|
|
246
|
+
as: "div",
|
|
247
|
+
role: "alert",
|
|
248
|
+
"aria-live": "polite"
|
|
249
|
+
}, renderScreenReaderAlert())), /*#__PURE__*/React.createElement(Flex, {
|
|
250
|
+
direction: "column",
|
|
251
|
+
height: getTrayHeight()
|
|
252
|
+
}, /*#__PURE__*/React.createElement(Flex.Item, {
|
|
253
|
+
padding: "medium medium small"
|
|
254
|
+
}, /*#__PURE__*/React.createElement(Flex, {
|
|
255
|
+
direction: "row"
|
|
256
|
+
}, /*#__PURE__*/React.createElement(Flex.Item, {
|
|
257
|
+
shouldGrow: true,
|
|
258
|
+
shouldShrink: true
|
|
259
|
+
}, /*#__PURE__*/React.createElement(Heading, {
|
|
260
|
+
as: "h2"
|
|
261
|
+
}, formatMessage('Find and Replace'))), /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(CloseButton, {
|
|
262
|
+
placement: "static",
|
|
263
|
+
color: "primary",
|
|
264
|
+
"data-testid": "close-button",
|
|
265
|
+
screenReaderLabel: formatMessage('Close'),
|
|
266
|
+
onClick: onRequestClose
|
|
267
|
+
})))), /*#__PURE__*/React.createElement(Flex.Item, {
|
|
268
|
+
as: "div",
|
|
269
|
+
padding: "0 large large"
|
|
270
|
+
}, /*#__PURE__*/React.createElement(View, {
|
|
271
|
+
as: "div",
|
|
272
|
+
margin: "large 0 medium 0"
|
|
273
|
+
}, /*#__PURE__*/React.createElement(TextInput, {
|
|
274
|
+
renderLabel: formatMessage('Find'),
|
|
275
|
+
name: "findtext",
|
|
276
|
+
onChange: e => handleTextChange(e),
|
|
277
|
+
onKeyDown: e => handleFindKeyDown(e),
|
|
278
|
+
value: findText,
|
|
279
|
+
placeholder: formatMessage('enter search text'),
|
|
280
|
+
renderAfterInput: resultText(),
|
|
281
|
+
messages: messages,
|
|
282
|
+
"data-testid": "find-text-input"
|
|
283
|
+
})), /*#__PURE__*/React.createElement(View, {
|
|
284
|
+
as: "div"
|
|
285
|
+
}, /*#__PURE__*/React.createElement(Flex, null, /*#__PURE__*/React.createElement(Flex.Item, {
|
|
286
|
+
size: "6.125rem"
|
|
287
|
+
}, /*#__PURE__*/React.createElement(Button, {
|
|
288
|
+
color: "secondary",
|
|
289
|
+
margin: "0 medium 0 0",
|
|
290
|
+
onClick: onPrevious,
|
|
291
|
+
disabled: isButtonDisabled('previous'),
|
|
292
|
+
"data-testid": "previous-button",
|
|
293
|
+
"aria-label": formatMessage('Previous {findText}', {
|
|
294
|
+
findText
|
|
295
|
+
})
|
|
296
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
297
|
+
"aria-hidden": "true"
|
|
298
|
+
}, formatMessage('Previous')))), /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(Button, {
|
|
299
|
+
color: "secondary",
|
|
300
|
+
margin: "0 small 0 0",
|
|
301
|
+
onClick: onNext,
|
|
302
|
+
disabled: isButtonDisabled('next'),
|
|
303
|
+
"data-testid": "next-button",
|
|
304
|
+
"aria-label": formatMessage('Next {findText}', {
|
|
305
|
+
findText
|
|
306
|
+
})
|
|
307
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
308
|
+
"aria-hidden": "true"
|
|
309
|
+
}, formatMessage('Next')))))), /*#__PURE__*/React.createElement(View, {
|
|
310
|
+
as: "div",
|
|
311
|
+
margin: "large 0 medium 0"
|
|
312
|
+
}, /*#__PURE__*/React.createElement(TextInput, {
|
|
313
|
+
renderLabel: formatMessage('Replace with'),
|
|
314
|
+
name: "replacetext",
|
|
315
|
+
onChange: e => setReplaceText(e.target.value),
|
|
316
|
+
onKeyDown: e => handleReplaceKeyDown(e),
|
|
317
|
+
value: replaceText,
|
|
318
|
+
placeholder: formatMessage('enter replacement text'),
|
|
319
|
+
"data-testid": "replace-text-input"
|
|
320
|
+
})), /*#__PURE__*/React.createElement(View, {
|
|
321
|
+
as: "div"
|
|
322
|
+
}, /*#__PURE__*/React.createElement(Flex, null, /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(Button, {
|
|
323
|
+
color: "secondary",
|
|
324
|
+
margin: "0 small 0 0",
|
|
325
|
+
onClick: () => replace(replaceText, true, true),
|
|
326
|
+
disabled: isButtonDisabled('replaceAll'),
|
|
327
|
+
"data-testid": "replace-all-button",
|
|
328
|
+
"aria-label": formatMessage('Replace all {findText} with {replaceText}', {
|
|
329
|
+
findText,
|
|
330
|
+
replaceText
|
|
331
|
+
})
|
|
332
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
333
|
+
"aria-hidden": "true"
|
|
334
|
+
}, formatMessage('Replace All')))), /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(Button, {
|
|
335
|
+
color: "secondary",
|
|
336
|
+
margin: "0 small 0 0",
|
|
337
|
+
onClick: () => {
|
|
338
|
+
replace(replaceText, true, false);
|
|
339
|
+
},
|
|
340
|
+
disabled: isButtonDisabled('replace'),
|
|
341
|
+
"data-testid": "replace-button",
|
|
342
|
+
"aria-label": formatMessage('Replace {findText} with {replaceText}', {
|
|
343
|
+
findText,
|
|
344
|
+
replaceText
|
|
345
|
+
})
|
|
346
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
347
|
+
"aria-hidden": "true"
|
|
348
|
+
}, formatMessage('Replace')))))))));
|
|
349
|
+
}
|
|
350
|
+
FindReplaceTray.propTypes = {
|
|
351
|
+
onNext: _pt.func.isRequired,
|
|
352
|
+
onPrevious: _pt.func.isRequired,
|
|
353
|
+
onFind: _pt.func.isRequired,
|
|
354
|
+
onRequestClose: _pt.func.isRequired,
|
|
355
|
+
onReplace: _pt.func.isRequired,
|
|
356
|
+
index: _pt.number.isRequired,
|
|
357
|
+
max: _pt.number.isRequired,
|
|
358
|
+
initialText: _pt.string,
|
|
359
|
+
selectionContext: _pt.arrayOf(_pt.string)
|
|
360
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import _pt from "prop-types";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (C) 2019 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* This file is part of Canvas.
|
|
7
|
+
*
|
|
8
|
+
* Canvas is free software: you can redistribute it and/or modify it under
|
|
9
|
+
* the terms of the GNU Affero General Public License as published by the Free
|
|
10
|
+
* Software Foundation, version 3 of the License.
|
|
11
|
+
*
|
|
12
|
+
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
13
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
14
|
+
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
15
|
+
* details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU Affero General Public License along
|
|
18
|
+
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
import React, { useState } from 'react';
|
|
21
|
+
import FindReplaceTray from './FindReplaceTray';
|
|
22
|
+
export default function FindReplaceTrayController(_ref) {
|
|
23
|
+
let {
|
|
24
|
+
plugin,
|
|
25
|
+
onDismiss,
|
|
26
|
+
initialText = '',
|
|
27
|
+
undoManager,
|
|
28
|
+
getSelectionContext
|
|
29
|
+
} = _ref;
|
|
30
|
+
// this component really just exists to make the index easier to track
|
|
31
|
+
const [findIndex, setFindIndex] = useState(0);
|
|
32
|
+
const [findCount, setFindCount] = useState(0);
|
|
33
|
+
const [selectionContext, setSelectionContext] = useState(['', '']);
|
|
34
|
+
|
|
35
|
+
const updateSelectionContext = () => {
|
|
36
|
+
const srText = getSelectionContext();
|
|
37
|
+
setSelectionContext(srText);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const newFindIndex = direction => {
|
|
41
|
+
let newIndex = 0;
|
|
42
|
+
|
|
43
|
+
if (findCount === 0) {
|
|
44
|
+
newIndex = 0;
|
|
45
|
+
} else if (direction === 1) {
|
|
46
|
+
// at max count, wrap back to 1
|
|
47
|
+
if (findIndex === findCount) {
|
|
48
|
+
newIndex = 1;
|
|
49
|
+
} else newIndex = findIndex + 1;
|
|
50
|
+
} else if (direction === -1) {
|
|
51
|
+
// at 1, wrap back to max
|
|
52
|
+
if (findIndex === 1) {
|
|
53
|
+
newIndex = findCount;
|
|
54
|
+
} else newIndex = findIndex - 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return newIndex;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const done = () => {
|
|
61
|
+
plugin.done(false);
|
|
62
|
+
setFindCount(0);
|
|
63
|
+
setFindIndex(0);
|
|
64
|
+
setSelectionContext([]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleDismiss = () => {
|
|
68
|
+
done();
|
|
69
|
+
onDismiss();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleNext = () => {
|
|
73
|
+
plugin.next();
|
|
74
|
+
setFindIndex(newFindIndex(1));
|
|
75
|
+
updateSelectionContext();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handlePrevious = () => {
|
|
79
|
+
plugin.prev();
|
|
80
|
+
setFindIndex(newFindIndex(-1));
|
|
81
|
+
updateSelectionContext();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleFind = text => {
|
|
85
|
+
if (!text) {
|
|
86
|
+
done();
|
|
87
|
+
} else {
|
|
88
|
+
const count = plugin.find(text);
|
|
89
|
+
setFindCount(count);
|
|
90
|
+
const index = count ? 1 : 0;
|
|
91
|
+
setFindIndex(index);
|
|
92
|
+
updateSelectionContext();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleReplace = function (text) {
|
|
97
|
+
let forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
98
|
+
let all = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
99
|
+
if (!text) return;
|
|
100
|
+
undoManager === null || undoManager === void 0 ? void 0 : undoManager.add();
|
|
101
|
+
plugin.replace(text, forward, all);
|
|
102
|
+
|
|
103
|
+
if (findCount === 1 || all) {
|
|
104
|
+
done();
|
|
105
|
+
return;
|
|
106
|
+
} // we can't just find again, because that will reset index to 1
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
let newIndex;
|
|
110
|
+
const newFindCount = findCount - 1;
|
|
111
|
+
|
|
112
|
+
if (forward) {
|
|
113
|
+
newIndex = findIndex === findCount ? 1 : findIndex;
|
|
114
|
+
} else {
|
|
115
|
+
newIndex = findIndex === 1 ? newFindCount : findIndex - 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setFindCount(newFindCount);
|
|
119
|
+
setFindIndex(newIndex);
|
|
120
|
+
updateSelectionContext();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return /*#__PURE__*/React.createElement(FindReplaceTray, {
|
|
124
|
+
onRequestClose: handleDismiss,
|
|
125
|
+
onNext: handleNext,
|
|
126
|
+
onPrevious: handlePrevious,
|
|
127
|
+
onFind: handleFind,
|
|
128
|
+
onReplace: handleReplace,
|
|
129
|
+
index: findIndex,
|
|
130
|
+
max: findCount,
|
|
131
|
+
initialText: initialText,
|
|
132
|
+
selectionContext: selectionContext
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
FindReplaceTrayController.propTypes = {
|
|
136
|
+
onDismiss: _pt.func.isRequired,
|
|
137
|
+
initialText: _pt.string,
|
|
138
|
+
getSelectionContext: _pt.func.isRequired
|
|
139
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
const NUMBER_OF_WORDS = 10;
|
|
19
|
+
|
|
20
|
+
const countWords = text => {
|
|
21
|
+
// ignore text before the first whitespace because it is part of the selected text
|
|
22
|
+
const countText = text.split(/\s+/).slice(1).join(' ');
|
|
23
|
+
const count = countText.trim().split(/\s+/).length;
|
|
24
|
+
return count;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getAfterText = startingElement => {
|
|
28
|
+
let text = '';
|
|
29
|
+
let element = startingElement.nextSibling;
|
|
30
|
+
|
|
31
|
+
while (element) {
|
|
32
|
+
text += element.textContent;
|
|
33
|
+
element = element.nextSibling;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (text.includes('.')) {
|
|
37
|
+
const index = text.indexOf('.');
|
|
38
|
+
text = text.substring(0, index + 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (countWords(text) > NUMBER_OF_WORDS) {
|
|
42
|
+
text = text.split(/\s+/).slice(0, NUMBER_OF_WORDS + 1).join(' ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return text;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getBeforeText = (startingElement, wordCount) => {
|
|
49
|
+
let text = '';
|
|
50
|
+
let element = startingElement.previousSibling;
|
|
51
|
+
|
|
52
|
+
while (element) {
|
|
53
|
+
text = element.textContent + text;
|
|
54
|
+
element = element.previousSibling;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
text = text.split(/\s+/).slice(-wordCount - 1).join(' ');
|
|
58
|
+
return text;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getSelectionContext = elements => {
|
|
62
|
+
const firstSelected = elements[0];
|
|
63
|
+
const lastSelected = elements[elements.length - 1];
|
|
64
|
+
const afterText = getAfterText(lastSelected);
|
|
65
|
+
const remainingWordCount = Math.max(NUMBER_OF_WORDS - countWords(afterText), 0);
|
|
66
|
+
const beforeText = getBeforeText(firstSelected, remainingWordCount);
|
|
67
|
+
return [beforeText, afterText];
|
|
68
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
import tinymce from 'tinymce';
|
|
19
|
+
import formatMessage from '../../../format-message';
|
|
20
|
+
import clickCallback from './clickCallback';
|
|
21
|
+
tinymce.PluginManager.add('instructure_search_and_replace', function (editor) {
|
|
22
|
+
var _editor$plugins;
|
|
23
|
+
|
|
24
|
+
// We use the searchreplace plugins API
|
|
25
|
+
if (!((_editor$plugins = editor.plugins) !== null && _editor$plugins !== void 0 && _editor$plugins.searchreplace)) return;
|
|
26
|
+
|
|
27
|
+
const launchFindModal = ed => () => {
|
|
28
|
+
clickCallback(ed, document);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
editor.addCommand('launch_instructure_search_and_replace', launchFindModal(editor));
|
|
32
|
+
editor.ui.registry.addMenuItem('instructure_search_and_replace', {
|
|
33
|
+
text: formatMessage('Find and Replace'),
|
|
34
|
+
icon: 'search',
|
|
35
|
+
shortcut: 'Meta+F',
|
|
36
|
+
onAction: () => editor.execCommand('launch_instructure_search_and_replace')
|
|
37
|
+
});
|
|
38
|
+
editor.shortcuts.add('Meta+F', '', launchFindModal(editor));
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -94,11 +94,13 @@ export function mediaPlayerURLFromFile(file, canvasOrigin) {
|
|
|
94
94
|
const type = content_type.replace(/\/.*$/, '');
|
|
95
95
|
|
|
96
96
|
if ((_RCEGlobals$getFeatur = RCEGlobals.getFeatures()) !== null && _RCEGlobals$getFeatur !== void 0 && _RCEGlobals$getFeatur.media_links_use_attachment_id && isAudioOrVideo(content_type) && file.id) {
|
|
97
|
+
var _RCEGlobals$getFeatur2;
|
|
98
|
+
|
|
97
99
|
const url = parse(`/media_attachments_iframe/${file.id}`, true);
|
|
98
100
|
url.query.type = type;
|
|
99
101
|
url.query.embedded = true;
|
|
100
102
|
|
|
101
|
-
if (file.uuid && file.contextType == 'User') {
|
|
103
|
+
if (file.uuid && (file.contextType == 'User' || !!canvasOrigin && canvasOrigin !== window.location.origin && (_RCEGlobals$getFeatur2 = RCEGlobals.getFeatures()) !== null && _RCEGlobals$getFeatur2 !== void 0 && _RCEGlobals$getFeatur2.file_verifiers_for_quiz_links)) {
|
|
102
104
|
url.query.verifier = file.uuid;
|
|
103
105
|
} else if (file.url || file.href) {
|
|
104
106
|
const parsed_url = parse(file.url || file.href, true);
|
|
@@ -34,6 +34,7 @@ tinymce.create('tinymce.plugins.AccessibilityChecker', {
|
|
|
34
34
|
config,
|
|
35
35
|
additionalRules,
|
|
36
36
|
mountNode,
|
|
37
|
+
triggerElementId,
|
|
37
38
|
onFixError
|
|
38
39
|
} = _ref;
|
|
39
40
|
|
|
@@ -43,7 +44,16 @@ tinymce.create('tinymce.plugins.AccessibilityChecker', {
|
|
|
43
44
|
editor: ed,
|
|
44
45
|
additionalRules: additionalRules,
|
|
45
46
|
mountNode: mountNode,
|
|
46
|
-
onClose: () =>
|
|
47
|
+
onClose: () => {
|
|
48
|
+
isCheckerOpen = false;
|
|
49
|
+
|
|
50
|
+
if (triggerElementId) {
|
|
51
|
+
var _button$;
|
|
52
|
+
|
|
53
|
+
const button = document.querySelectorAll(`[data-btn-id=${triggerElementId}]`);
|
|
54
|
+
(_button$ = button[0]) === null || _button$ === void 0 ? void 0 : _button$.focus();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
47
57
|
onFixError: onFixError
|
|
48
58
|
}), container, function () {
|
|
49
59
|
// this is a workaround for react 16 since ReactDOM.render is not
|
|
@@ -28,7 +28,7 @@ export const A11Y_CHECKER_STYLE_ELEM_ID = 'a11y-checker-style'; // Remove the cu
|
|
|
28
28
|
// the style element
|
|
29
29
|
|
|
30
30
|
export function clearIndicators(doc) {
|
|
31
|
-
const checker_style = doc.getElementById(A11Y_CHECKER_STYLE_ELEM_ID);
|
|
31
|
+
const checker_style = doc === null || doc === void 0 ? void 0 : doc.getElementById(A11Y_CHECKER_STYLE_ELEM_ID);
|
|
32
32
|
|
|
33
33
|
if (checker_style) {
|
|
34
34
|
checker_style.textContent = '';
|
package/es/rce/tinyRCE.js
CHANGED
|
@@ -34,7 +34,8 @@ import 'tinymce/plugins/textpattern/plugin';
|
|
|
34
34
|
import 'tinymce/plugins/wordcount/plugin';
|
|
35
35
|
import 'tinymce/plugins/paste/plugin';
|
|
36
36
|
import 'tinymce/plugins/table/plugin';
|
|
37
|
-
import 'tinymce/plugins/hr/plugin';
|
|
37
|
+
import 'tinymce/plugins/hr/plugin';
|
|
38
|
+
import 'tinymce/plugins/searchreplace/plugin'; // add custom plugins
|
|
38
39
|
|
|
39
40
|
import './plugins/instructure-ui-icons/plugin';
|
|
40
41
|
import './plugins/instructure_condensed_buttons/plugin';
|
|
@@ -51,6 +52,7 @@ import './plugins/instructure_wordcount/plugin';
|
|
|
51
52
|
import './plugins/instructure_paste/plugin';
|
|
52
53
|
import './plugins/instructure_fullscreen/plugin';
|
|
53
54
|
import './plugins/instructure_studio_media_options/plugin';
|
|
55
|
+
import './plugins/instructure_search_and_replace/plugin';
|
|
54
56
|
import './plugins/tinymce-a11y-checker/plugin'; // prevent tinymce from loading language scripts with explicit
|
|
55
57
|
// language_url of 'none'
|
|
56
58
|
|