diff --git a/ui-build/webpack/esmac/errorsPendingRemoval.json b/ui-build/webpack/esmac/errorsPendingRemoval.json
index cd8ffcba36c..814c3da871b 100644
--- a/ui-build/webpack/esmac/errorsPendingRemoval.json
+++ b/ui-build/webpack/esmac/errorsPendingRemoval.json
@@ -125,12 +125,6 @@
"target": "ui/shared/datetime/changeTimezone.js",
"request": "@canvas/datetime/changeTimezone"
},
- {
- "name": "SpecifierMismatchError",
- "source": "ui/shared/lti/jquery/messages.js",
- "target": "ui/shared/rce/plugins/canvas_mentions/constants.js",
- "request": "../../rce/plugins/canvas_mentions/constants"
- },
{
"name": "SpecifierMismatchError",
"source": "ui/shared/outcomes/react/hooks/useGroupCreate.js",
diff --git a/ui/shared/lti/jquery/lti_message_handler.ts b/ui/shared/lti/jquery/lti_message_handler.ts
new file mode 100644
index 00000000000..42fa028817f
--- /dev/null
+++ b/ui/shared/lti/jquery/lti_message_handler.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import {ResponseMessages} from './response_messages'
+
+export interface LtiMessageHandler {
+ /**
+ * /**
+ * A handler for a single type of LTI postMessage message
+ * @param params includes the message
+ * @returns true if the handler has already sent a response
+ */
+ (params: {message: T; event: MessageEvent; responseMessages: ResponseMessages}): boolean
+}
diff --git a/ui/shared/lti/jquery/messages.js b/ui/shared/lti/jquery/messages.js
deleted file mode 100644
index 800937d38d0..00000000000
--- a/ui/shared/lti/jquery/messages.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2018 - present Instructure, Inc.
- *
- * This file is part of Canvas.
- *
- * Canvas is free software: you can redistribute it and/or modify it under
- * the terms of the GNU Affero General Public License as published by the Free
- * Software Foundation, version 3 of the License.
- *
- * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
- * details.
- *
- * You should have received a copy of the GNU Affero General Public License along
- * with this program. If not, see .
- */
-
-/* eslint no-console: 0 */
-
-import {
- NAVIGATION_MESSAGE as MENTIONS_NAVIGATION_MESSAGE,
- INPUT_CHANGE_MESSAGE as MENTIONS_INPUT_CHANGE_MESSAGE,
- SELECTION_MESSAGE as MENTIONS_SELECTION_MESSAGE,
-} from '../../rce/plugins/canvas_mentions/constants'
-import buildResponseMessages from './response_messages'
-
-// page-global storage for data relevant to LTI postMessage events
-const ltiState = {}
-
-const SUBJECT_ALLOW_LIST = [
- 'lti.enableScrollEvents',
- 'lti.fetchWindowSize',
- 'lti.frameResize',
- 'lti.hideRightSideWrapper',
- 'lti.removeUnloadMessage',
- 'lti.resourceImported',
- 'lti.screenReaderAlert',
- 'lti.scrollToTop',
- 'lti.setUnloadMessage',
- 'lti.showAlert',
- 'lti.showModuleNavigation',
- 'org.imsglobal.lti.capabilities',
- 'org.imsglobal.lti.get_data',
- 'org.imsglobal.lti.put_data',
- 'requestFullWindowLaunch',
- 'toggleCourseNavigationMenu',
-]
-
-// These are handled elsewhere so ignore them
-const SUBJECT_IGNORE_LIST = [
- 'A2ExternalContentReady',
- 'LtiDeepLinkingResponse',
- 'externalContentReady',
- 'externalContentCancel',
- MENTIONS_NAVIGATION_MESSAGE,
- MENTIONS_INPUT_CHANGE_MESSAGE,
- MENTIONS_SELECTION_MESSAGE,
- 'betterchat.is_mini_chat',
-]
-
-async function ltiMessageHandler(e) {
- if (e.data?.source?.includes('react-devtools') || e.data.isAngularDevTools) {
- return false
- }
-
- let message
- try {
- message = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
- } catch (err) {
- // unparseable message may not be meant for our handlers
- return false
- }
-
- // look at messageType for backwards compatibility
- const subject = message.subject || message.messageType
- const responseMessages = buildResponseMessages({
- targetWindow: e.source,
- origin: e.origin,
- subject,
- message_id: message.message_id,
- toolOrigin: message.toolOrigin,
- })
-
- if (
- SUBJECT_IGNORE_LIST.includes(subject) ||
- subject === undefined ||
- responseMessages.isResponse(e)
- ) {
- // These messages are handled elsewhere
- return false
- } else if (!SUBJECT_ALLOW_LIST.includes(subject)) {
- // Enforce subject allowlist -- unknown type
- responseMessages.sendUnsupportedSubjectError()
- return false
- }
-
- try {
- const handlerModule = await import(
- /* webpackExclude: /__tests__/ */
- `./subjects/${subject}.js`
- )
- const hasSentResponse = handlerModule.default({
- message,
- event: e,
- responseMessages,
- })
- if (!hasSentResponse) {
- responseMessages.sendSuccess()
- }
- return true
- } catch (error) {
- console.error(`Error loading or executing message handler for "${subject}": ${error}`)
- responseMessages.sendGenericError(error.message)
- return false
- }
-}
-
-let hasListener = false
-
-function monitorLtiMessages() {
- const cb = e => {
- if (e.data !== '') ltiMessageHandler(e)
- }
- if (!hasListener) {
- window.addEventListener('message', cb)
- hasListener = true
- }
-}
-
-export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}
diff --git a/ui/shared/lti/jquery/messages.ts b/ui/shared/lti/jquery/messages.ts
new file mode 100644
index 00000000000..dac07c71e92
--- /dev/null
+++ b/ui/shared/lti/jquery/messages.ts
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2018 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+/* eslint no-console: 0 */
+
+import {
+ NAVIGATION_MESSAGE as MENTIONS_NAVIGATION_MESSAGE,
+ INPUT_CHANGE_MESSAGE as MENTIONS_INPUT_CHANGE_MESSAGE,
+ SELECTION_MESSAGE as MENTIONS_SELECTION_MESSAGE,
+} from '@canvas/rce/plugins/canvas_mentions/constants'
+import {LtiMessageHandler} from './lti_message_handler'
+import buildResponseMessages from './response_messages'
+import {getKey, hasKey} from './util'
+
+// page-global storage for data relevant to LTI postMessage events
+const ltiState: {
+ tray?: {refreshOnClose?: boolean}
+ fullWindowProxy?: Window | null
+} = {}
+
+const SUBJECT_ALLOW_LIST = [
+ 'lti.enableScrollEvents',
+ 'lti.fetchWindowSize',
+ 'lti.frameResize',
+ 'lti.hideRightSideWrapper',
+ 'lti.removeUnloadMessage',
+ 'lti.resourceImported',
+ 'lti.screenReaderAlert',
+ 'lti.scrollToTop',
+ 'lti.setUnloadMessage',
+ 'lti.showAlert',
+ 'lti.showModuleNavigation',
+ 'org.imsglobal.lti.capabilities',
+ 'org.imsglobal.lti.get_data',
+ 'org.imsglobal.lti.put_data',
+ 'requestFullWindowLaunch',
+ 'toggleCourseNavigationMenu',
+] as const
+
+type SubjectId = typeof SUBJECT_ALLOW_LIST[number]
+
+const isAllowedSubject = (subject: unknown): subject is SubjectId =>
+ typeof subject === 'string' && (SUBJECT_ALLOW_LIST as ReadonlyArray).includes(subject)
+
+const isIgnoredSubject = (subject: unknown): subject is SubjectId =>
+ typeof subject === 'string' && (SUBJECT_IGNORE_LIST as ReadonlyArray).includes(subject)
+
+// These are handled elsewhere so ignore them
+const SUBJECT_IGNORE_LIST = [
+ 'A2ExternalContentReady',
+ 'LtiDeepLinkingResponse',
+ 'externalContentReady',
+ 'externalContentCancel',
+ MENTIONS_NAVIGATION_MESSAGE,
+ MENTIONS_INPUT_CHANGE_MESSAGE,
+ MENTIONS_SELECTION_MESSAGE,
+ 'betterchat.is_mini_chat',
+] as const
+
+const isObject = (u: unknown): u is object => {
+ return typeof u === 'object'
+}
+
+/**
+ * Returns true if the data from a message event is associated with a dev tool
+ * @param data - The `data` attribute from a message event
+ */
+const isDevtoolMessageData = (data: unknown): boolean => {
+ return (
+ isObject(data) &&
+ ((hasKey('source', data) &&
+ typeof data.source === 'string' &&
+ data.source.includes('react-devtools')) ||
+ (hasKey('isAngularDevTools', data) && !!data.isAngularDevTools))
+ )
+}
+
+/**
+ * A mapping of lti message id to a function that "handles" the message
+ * The values are functions to preserve the asynchronous loading of
+ * code that was present in the previous style. It may not be necessary.
+ */
+const handlers: Record<
+ typeof SUBJECT_ALLOW_LIST[number],
+ () => Promise<{default: LtiMessageHandler}>
+> = {
+ 'lti.enableScrollEvents': () => import(`./subjects/lti.enableScrollEvents`),
+ 'lti.fetchWindowSize': () => import(`./subjects/lti.fetchWindowSize`),
+ 'lti.frameResize': () => import(`./subjects/lti.frameResize`),
+ 'lti.hideRightSideWrapper': () => import(`./subjects/lti.hideRightSideWrapper`),
+ 'lti.removeUnloadMessage': () => import(`./subjects/lti.removeUnloadMessage`),
+ 'lti.resourceImported': () => import(`./subjects/lti.resourceImported`),
+ 'lti.screenReaderAlert': () => import(`./subjects/lti.screenReaderAlert`),
+ 'lti.scrollToTop': () => import(`./subjects/lti.scrollToTop`),
+ 'lti.setUnloadMessage': () => import(`./subjects/lti.setUnloadMessage`),
+ 'lti.showAlert': () => import(`./subjects/lti.showAlert`),
+ 'lti.showModuleNavigation': () => import(`./subjects/lti.showModuleNavigation`),
+ 'org.imsglobal.lti.capabilities': () => import(`./subjects/org.imsglobal.lti.capabilities`),
+ 'org.imsglobal.lti.get_data': () => import(`./subjects/org.imsglobal.lti.get_data`),
+ 'org.imsglobal.lti.put_data': () => import(`./subjects/org.imsglobal.lti.put_data`),
+ requestFullWindowLaunch: () => import(`./subjects/requestFullWindowLaunch`),
+ toggleCourseNavigationMenu: () => import(`./subjects/toggleCourseNavigationMenu`),
+}
+
+/**
+ * Handles 'message' events for LTI-related messages from LTI tools
+ * @param e
+ * @returns
+ */
+async function ltiMessageHandler(e: MessageEvent) {
+ if (isDevtoolMessageData(e.data)) {
+ return false
+ }
+
+ let message: unknown
+ try {
+ message = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
+ } catch (err) {
+ // unparseable message may not be meant for our handlers
+ return false
+ }
+
+ if (typeof message !== 'object' || message === null) {
+ // unparseable message may not be meant for our handlers
+ return false
+ }
+
+ const targetWindow = e.source as Window
+
+ // look at messageType for backwards compatibility
+ const subject = getKey('subject', message) || getKey('messageType', message)
+ const responseMessages = buildResponseMessages({
+ targetWindow,
+ origin: e.origin,
+ subject,
+ message_id: getKey('message_id', message),
+ toolOrigin: getKey('toolOrigin', message),
+ })
+
+ if (subject === undefined || isIgnoredSubject(subject) || responseMessages.isResponse(e)) {
+ // These messages are handled elsewhere
+ return false
+ } else if (!isAllowedSubject(subject)) {
+ responseMessages.sendUnsupportedSubjectError()
+ return false
+ } else {
+ try {
+ const handlerModule = await handlers[subject]()
+ const hasSentResponse = handlerModule.default({
+ message,
+ event: e,
+ responseMessages,
+ })
+ if (!hasSentResponse) {
+ responseMessages.sendSuccess()
+ }
+ return true
+ } catch (error) {
+ console.error(`Error loading or executing message handler for "${subject}": ${error}`)
+
+ const message =
+ isObject(error) && hasKey('message', error) && typeof error.message === 'string'
+ ? error.message
+ : undefined
+ responseMessages.sendGenericError(message)
+ return false
+ }
+ }
+}
+
+let hasListener = false
+
+function monitorLtiMessages() {
+ const cb = (e: MessageEvent) => {
+ if (e.data !== '') ltiMessageHandler(e)
+ }
+ if (!hasListener) {
+ window.addEventListener('message', cb)
+ hasListener = true
+ }
+}
+
+export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}
diff --git a/ui/shared/lti/jquery/platform_storage.js b/ui/shared/lti/jquery/platform_storage.ts
similarity index 75%
rename from ui/shared/lti/jquery/platform_storage.js
rename to ui/shared/lti/jquery/platform_storage.ts
index cb067aa1f80..50f9257c1ce 100644
--- a/ui/shared/lti/jquery/platform_storage.js
+++ b/ui/shared/lti/jquery/platform_storage.ts
@@ -32,22 +32,22 @@ const getLimit = tool_id => {
return limits[tool_id]
}
-const clearLimit = tool_id => {
+const clearLimit = (tool_id: string) => {
delete limits[tool_id]
}
-const addToLimit = (tool_id, key, value) => {
+const addToLimit = (tool_id: string, key: string, value: string) => {
createLimit(tool_id)
const length = key.length + value.length
if (limits[tool_id].keyCount >= STORAGE_KEY_LIMIT) {
- const e = new Error('Reached key limit for tool')
+ const e: Error & {code?: string} = new Error('Reached key limit for tool')
e.code = 'storage_exhaustion'
throw e
}
if (limits[tool_id].charCount + length > STORAGE_CHAR_LIMIT) {
- const e = new Error('Reached byte limit for tool')
+ const e: Error & {code?: string} = new Error('Reached byte limit for tool')
e.code = 'storage_exhaustion'
throw e
}
@@ -56,7 +56,7 @@ const addToLimit = (tool_id, key, value) => {
limits[tool_id].charCount += length
}
-const removeFromLimit = (tool_id, key, value) => {
+const removeFromLimit = (tool_id: string, key: string, value: string) => {
limits[tool_id].keyCount--
limits[tool_id].charCount -= key.length + value.length
@@ -68,16 +68,16 @@ const removeFromLimit = (tool_id, key, value) => {
}
}
-const getKey = (tool_id, key) => `lti|platform_storage|${tool_id}|${key}`
+const getKey = (tool_id: string, key: string) => `lti|platform_storage|${tool_id}|${key}`
-const putData = (tool_id, key, value) => {
+const putData = (tool_id: string, key: string, value: string) => {
addToLimit(tool_id, key, value)
window.localStorage.setItem(getKey(tool_id, key), value)
}
-const getData = (tool_id, key) => window.localStorage.getItem(getKey(tool_id, key))
+const getData = (tool_id: string, key: string) => window.localStorage.getItem(getKey(tool_id, key))
-const clearData = (tool_id, key) => {
+const clearData = (tool_id: string, key: string) => {
const value = getData(tool_id, key)
if (value) {
removeFromLimit(tool_id, key, value)
diff --git a/ui/shared/lti/jquery/response_messages.js b/ui/shared/lti/jquery/response_messages.ts
similarity index 68%
rename from ui/shared/lti/jquery/response_messages.js
rename to ui/shared/lti/jquery/response_messages.ts
index a7e0d2ce053..ea5f8890777 100644
--- a/ui/shared/lti/jquery/response_messages.js
+++ b/ui/shared/lti/jquery/response_messages.ts
@@ -21,9 +21,36 @@ const UNSUPPORTED_SUBJECT_ERROR_CODE = 'unsupported_subject'
const WRONG_ORIGIN_ERROR_CODE = 'wrong_origin'
const BAD_REQUEST_ERROR_CODE = 'bad_request'
-const buildResponseMessages = ({targetWindow, origin, subject, message_id, toolOrigin}) => {
+export interface ResponseMessages {
+ sendResponse: (contents?: {}) => void
+ sendSuccess: () => void
+ sendError: (code: string, message?: string | undefined) => void
+ sendGenericError: (message?: string | undefined) => void
+ sendBadRequestError: (message: any) => void
+ sendWrongOriginError: () => void
+ sendUnsupportedSubjectError: () => void
+ isResponse: (message: any) => boolean
+}
+
+const buildResponseMessages = ({
+ targetWindow,
+ origin,
+ subject,
+ message_id,
+ toolOrigin,
+}: {
+ targetWindow: Window | null
+ origin: string
+ subject: unknown
+ message_id: unknown
+ toolOrigin: unknown
+}): ResponseMessages => {
const sendResponse = (contents = {}) => {
- const message = {subject: `${subject}.response`}
+ const message: {
+ subject: string
+ message_id?: unknown
+ toolOrigin?: unknown
+ } = {subject: `${subject}.response`}
if (message_id) {
message.message_id = message_id
}
@@ -42,15 +69,15 @@ const buildResponseMessages = ({targetWindow, origin, subject, message_id, toolO
sendResponse({})
}
- const sendError = (code, message) => {
- const error = {code}
+ const sendError = (code: string, message?: string) => {
+ const error: {code: string; message?: string} = {code}
if (message) {
error.message = message
}
sendResponse({error})
}
- const sendGenericError = message => {
+ const sendGenericError = (message?: string) => {
sendError(GENERIC_ERROR_CODE, message)
}
diff --git a/ui/shared/lti/jquery/subjects/lti.enableScrollEvents.js b/ui/shared/lti/jquery/subjects/lti.enableScrollEvents.ts
similarity index 87%
rename from ui/shared/lti/jquery/subjects/lti.enableScrollEvents.js
rename to ui/shared/lti/jquery/subjects/lti.enableScrollEvents.ts
index 828f924354c..1c0fa45db95 100644
--- a/ui/shared/lti/jquery/subjects/lti.enableScrollEvents.js
+++ b/ui/shared/lti/jquery/subjects/lti.enableScrollEvents.ts
@@ -16,7 +16,9 @@
* with this program. If not, see .
*/
-export default function enableScrollEvents({responseMessages}) {
+import {LtiMessageHandler} from '../lti_message_handler'
+
+const enableScrollEvents: LtiMessageHandler = ({responseMessages}) => {
let timeout
window.addEventListener(
'scroll',
@@ -37,3 +39,5 @@ export default function enableScrollEvents({responseMessages}) {
)
return true
}
+
+export default enableScrollEvents
diff --git a/ui/shared/lti/jquery/subjects/lti.fetchWindowSize.js b/ui/shared/lti/jquery/subjects/lti.fetchWindowSize.ts
similarity index 85%
rename from ui/shared/lti/jquery/subjects/lti.fetchWindowSize.js
rename to ui/shared/lti/jquery/subjects/lti.fetchWindowSize.ts
index 0c5bfab745d..762c79a21fe 100644
--- a/ui/shared/lti/jquery/subjects/lti.fetchWindowSize.js
+++ b/ui/shared/lti/jquery/subjects/lti.fetchWindowSize.ts
@@ -17,8 +17,9 @@
*/
import $ from 'jquery'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function fetchWindowSize({responseMessages}) {
+const fetchWindowSize: LtiMessageHandler = ({responseMessages}) => {
responseMessages.sendResponse({
height: window.innerHeight,
width: window.innerWidth,
@@ -28,3 +29,5 @@ export default function fetchWindowSize({responseMessages}) {
})
return true
}
+
+export default fetchWindowSize
diff --git a/ui/shared/lti/jquery/subjects/lti.frameResize.js b/ui/shared/lti/jquery/subjects/lti.frameResize.ts
similarity index 75%
rename from ui/shared/lti/jquery/subjects/lti.frameResize.js
rename to ui/shared/lti/jquery/subjects/lti.frameResize.ts
index 590bfa74842..a7db49ba3e6 100644
--- a/ui/shared/lti/jquery/subjects/lti.frameResize.js
+++ b/ui/shared/lti/jquery/subjects/lti.frameResize.ts
@@ -18,10 +18,14 @@
import ToolLaunchResizer from '../tool_launch_resizer'
import {findDomForWindow} from '../util'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function frameResize({message, event}) {
+const frameResize: LtiMessageHandler<{height: number | string; token: string}> = ({
+ message,
+ event,
+}) => {
const toolResizer = new ToolLaunchResizer()
- let height = message.height
+ let height: number | string = message.height as number | string
if (height <= 0) height = 1
const container = toolResizer
@@ -34,11 +38,12 @@ export default function frameResize({message, event}) {
// Attempt to find an embedded iframe that matches the event source.
const iframe = findDomForWindow(event.source)
if (iframe) {
- if (typeof height === 'number') {
- height += 'px'
- }
- iframe.height = height
- iframe.style.height = height
+ const strHeight = typeof height === 'number' ? `${height}px` : height
+ iframe.height = strHeight
+ iframe.style.height = strHeight
}
}
+ return false
}
+
+export default frameResize
diff --git a/ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.js b/ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.ts
similarity index 82%
rename from ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.js
rename to ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.ts
index 6ad979b0322..30274cf258e 100644
--- a/ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.js
+++ b/ui/shared/lti/jquery/subjects/lti.hideRightSideWrapper.ts
@@ -17,7 +17,11 @@
*/
import $ from 'jquery'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function hideRightSideWrapper() {
+const hideRightSideWrapper: LtiMessageHandler = () => {
$('#right-side-wrapper').hide()
+ return false
}
+
+export default hideRightSideWrapper
diff --git a/ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.js b/ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.ts
similarity index 84%
rename from ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.js
rename to ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.ts
index 6a8e14eb956..fb333704efb 100644
--- a/ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.js
+++ b/ui/shared/lti/jquery/subjects/lti.removeUnloadMessage.ts
@@ -17,7 +17,11 @@
*/
import {removeUnloadMessage} from '../util'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function remove() {
+const remove: LtiMessageHandler = () => {
removeUnloadMessage()
+ return false
}
+
+export default remove
diff --git a/ui/shared/lti/jquery/subjects/lti.resourceImported.js b/ui/shared/lti/jquery/subjects/lti.resourceImported.ts
similarity index 88%
rename from ui/shared/lti/jquery/subjects/lti.resourceImported.js
rename to ui/shared/lti/jquery/subjects/lti.resourceImported.ts
index d73588a3099..626a068fef2 100644
--- a/ui/shared/lti/jquery/subjects/lti.resourceImported.js
+++ b/ui/shared/lti/jquery/subjects/lti.resourceImported.ts
@@ -16,13 +16,15 @@
* with this program. If not, see .
*/
+import {LtiMessageHandler} from '../lti_message_handler'
import {ltiState} from '../messages'
-const handler = () => {
+const handler: LtiMessageHandler = () => {
if (!ltiState.tray) {
ltiState.tray = {}
}
ltiState.tray.refreshOnClose = true
+ return false
}
export default handler
diff --git a/ui/shared/lti/jquery/subjects/lti.screenReaderAlert.js b/ui/shared/lti/jquery/subjects/lti.screenReaderAlert.ts
similarity index 81%
rename from ui/shared/lti/jquery/subjects/lti.screenReaderAlert.js
rename to ui/shared/lti/jquery/subjects/lti.screenReaderAlert.ts
index 0e78f753d6a..46af6e1e41b 100644
--- a/ui/shared/lti/jquery/subjects/lti.screenReaderAlert.js
+++ b/ui/shared/lti/jquery/subjects/lti.screenReaderAlert.ts
@@ -17,9 +17,15 @@
*/
import $ from '@canvas/rails-flash-notifications'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function screenReaderAlert({message}) {
+const screenReaderAlert: LtiMessageHandler<{
+ body: string | unknown
+}> = ({message}) => {
$.screenReaderFlashMessageExclusive(
typeof message.body === 'string' ? message.body : JSON.stringify(message.body)
)
+ return false
}
+
+export default screenReaderAlert
diff --git a/ui/shared/lti/jquery/subjects/lti.scrollToTop.js b/ui/shared/lti/jquery/subjects/lti.scrollToTop.ts
similarity index 79%
rename from ui/shared/lti/jquery/subjects/lti.scrollToTop.js
rename to ui/shared/lti/jquery/subjects/lti.scrollToTop.ts
index a49083c2c8f..e2668b0bf7a 100644
--- a/ui/shared/lti/jquery/subjects/lti.scrollToTop.js
+++ b/ui/shared/lti/jquery/subjects/lti.scrollToTop.ts
@@ -17,12 +17,16 @@
*/
import $ from 'jquery'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function scrollToTop() {
+const scrollToTop: LtiMessageHandler = () => {
$('html,body').animate(
{
- scrollTop: $('.tool_content_wrapper').offset().top,
+ scrollTop: $('.tool_content_wrapper').offset()?.top,
},
'fast'
)
+ return false
}
+
+export default scrollToTop
diff --git a/ui/shared/lti/jquery/subjects/lti.setUnloadMessage.js b/ui/shared/lti/jquery/subjects/lti.setUnloadMessage.ts
similarity index 83%
rename from ui/shared/lti/jquery/subjects/lti.setUnloadMessage.js
rename to ui/shared/lti/jquery/subjects/lti.setUnloadMessage.ts
index 77cba40ccf8..84fd02026fd 100644
--- a/ui/shared/lti/jquery/subjects/lti.setUnloadMessage.js
+++ b/ui/shared/lti/jquery/subjects/lti.setUnloadMessage.ts
@@ -18,7 +18,11 @@
import htmlEscape from 'html-escape'
import {setUnloadMessage} from '../util'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function set({message}) {
+const set: LtiMessageHandler<{message: string}> = ({message}) => {
setUnloadMessage(htmlEscape(message.message))
+ return false
}
+
+export default set
diff --git a/ui/shared/lti/jquery/subjects/lti.showAlert.js b/ui/shared/lti/jquery/subjects/lti.showAlert.ts
similarity index 88%
rename from ui/shared/lti/jquery/subjects/lti.showAlert.js
rename to ui/shared/lti/jquery/subjects/lti.showAlert.ts
index e0f0aa084ca..06b7702b1b8 100644
--- a/ui/shared/lti/jquery/subjects/lti.showAlert.js
+++ b/ui/shared/lti/jquery/subjects/lti.showAlert.ts
@@ -18,10 +18,15 @@
import $ from '@canvas/rails-flash-notifications'
import {useScope as useI18nScope} from '@canvas/i18n'
+import {LtiMessageHandler} from '../lti_message_handler'
const I18n = useI18nScope('ltiMessages')
-export default function showAlert({message, responseMessages}) {
+const showAlert: LtiMessageHandler<{
+ body: unknown
+ title?: string
+ alertType?: string
+}> = ({message, responseMessages}) => {
if (!message.body) {
responseMessages.sendBadRequestError("Missing required 'body' field")
return true
@@ -49,3 +54,5 @@ export default function showAlert({message, responseMessages}) {
responseMessages.sendSuccess()
return true
}
+
+export default showAlert
diff --git a/ui/shared/lti/jquery/subjects/lti.showModuleNavigation.js b/ui/shared/lti/jquery/subjects/lti.showModuleNavigation.ts
similarity index 81%
rename from ui/shared/lti/jquery/subjects/lti.showModuleNavigation.js
rename to ui/shared/lti/jquery/subjects/lti.showModuleNavigation.ts
index 2645cfff2ae..ee6312ee0fc 100644
--- a/ui/shared/lti/jquery/subjects/lti.showModuleNavigation.js
+++ b/ui/shared/lti/jquery/subjects/lti.showModuleNavigation.ts
@@ -17,9 +17,15 @@
*/
import $ from 'jquery'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function showModuleNavigation({message}) {
+const showModuleNavigation: LtiMessageHandler<{
+ show?: boolean
+}> = ({message}) => {
if (message.show === true || message.show === false) {
$('.module-sequence-footer').toggle(message.show)
}
+ return false
}
+
+export default showModuleNavigation
diff --git a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.js b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.ts
similarity index 88%
rename from ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.js
rename to ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.ts
index 2cddf76fc6d..cc63be302e9 100644
--- a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.js
+++ b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.capabilities.ts
@@ -16,9 +16,10 @@
* with this program. If not, see .
*/
+import {LtiMessageHandler} from '../lti_message_handler'
import {SUBJECT_ALLOW_LIST} from '../messages'
-export default ({responseMessages}) => {
+const handler: LtiMessageHandler = ({responseMessages}) => {
const useFrame = ENV?.FEATURES?.lti_platform_storage
const imsSubjects = ['org.imsglobal.lti.get_data', 'org.imsglobal.lti.put_data']
const supported_messages = SUBJECT_ALLOW_LIST.map(subject => {
@@ -34,3 +35,5 @@ export default ({responseMessages}) => {
responseMessages.sendResponse({supported_messages})
return true
}
+
+export default handler
diff --git a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.js b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.ts
similarity index 84%
rename from ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.js
rename to ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.ts
index e30d1dcf126..4ae9924b538 100644
--- a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.js
+++ b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.get_data.ts
@@ -17,8 +17,12 @@
*/
import {getData} from '../platform_storage'
+import {LtiMessageHandler} from '../lti_message_handler'
-export default function handler({message, responseMessages, event}) {
+const handler: LtiMessageHandler<{
+ key: string
+ message_id: string
+}> = ({message, responseMessages, event}) => {
const {key, message_id} = message
if (!key) {
@@ -35,3 +39,5 @@ export default function handler({message, responseMessages, event}) {
responseMessages.sendResponse({key, value})
return true
}
+
+export default handler
diff --git a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.js b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.ts
similarity index 71%
rename from ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.js
rename to ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.ts
index 1dad340c85e..d92e4e54b42 100644
--- a/ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.js
+++ b/ui/shared/lti/jquery/subjects/org.imsglobal.lti.put_data.ts
@@ -17,8 +17,14 @@
*/
import {clearData, putData} from '../platform_storage'
+import {LtiMessageHandler} from '../lti_message_handler'
+import {getKey} from '../util'
-export default function handler({message, responseMessages, event}) {
+const handler: LtiMessageHandler<{
+ key: string
+ value: string
+ message_id: string
+}> = ({message, responseMessages, event}) => {
const {key, value, message_id} = message
if (!key) {
@@ -35,8 +41,13 @@ export default function handler({message, responseMessages, event}) {
try {
putData(event.origin, key, value)
responseMessages.sendResponse({key, value})
- } catch (e) {
- responseMessages.sendError(e.code, e.message)
+ } catch (e: unknown) {
+ const code = getKey('code', e)
+ const message = getKey('message', e)
+ responseMessages.sendError(
+ typeof code === 'string' ? code : '',
+ typeof message === 'string' ? message : undefined
+ )
}
} else {
clearData(event.origin, key)
@@ -44,3 +55,5 @@ export default function handler({message, responseMessages, event}) {
}
return true
}
+
+export default handler
diff --git a/ui/shared/lti/jquery/subjects/requestFullWindowLaunch.js b/ui/shared/lti/jquery/subjects/requestFullWindowLaunch.ts
similarity index 72%
rename from ui/shared/lti/jquery/subjects/requestFullWindowLaunch.js
rename to ui/shared/lti/jquery/subjects/requestFullWindowLaunch.ts
index 4e43d59de4a..93ecaecd7a0 100644
--- a/ui/shared/lti/jquery/subjects/requestFullWindowLaunch.js
+++ b/ui/shared/lti/jquery/subjects/requestFullWindowLaunch.ts
@@ -15,9 +15,33 @@
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see .
*/
+import {LtiMessageHandler} from '../lti_message_handler'
import {ltiState} from '../messages'
+import {hasKey} from '../util'
-const parseData = data => {
+const parseData = (
+ data:
+ | {
+ url?: string
+ display?: string
+ launchType?: string
+ launchOptions?: {
+ width?: number
+ height?: number
+ }
+ placement?: string
+ }
+ | string
+): {
+ url: string
+ display: string
+ launchType: string
+ launchOptions: {
+ width?: number
+ height?: number
+ }
+ placement?: string
+} => {
const defaults = {
display: 'borderless',
launchType: 'same_window',
@@ -28,20 +52,24 @@ const parseData = data => {
url: data,
...defaults,
}
- } else if (typeof data === 'object' && !(data instanceof Array)) {
- if (!data.url) {
- throw new Error('message must contain a `url` property')
- }
- return {
- ...defaults,
- ...data,
+ } else if (typeof data === 'object' && !(data instanceof Array) && data !== null) {
+ if (hasKey('url', data)) {
+ const url = data.url
+ if (typeof url === 'string') {
+ return {
+ ...defaults,
+ ...data,
+ url,
+ }
+ }
}
+ throw new Error('message must contain a `url` property')
} else {
throw new Error('message contents must either be a string or an object')
}
}
-const buildLaunchUrl = (messageUrl, placement, display) => {
+const buildLaunchUrl = (messageUrl: string, placement: string | undefined, display: string) => {
let context = ENV.context_asset_string.replace('_', 's/')
if (!(context.startsWith('account') || context.startsWith('course'))) {
context = 'accounts/' + ENV.DOMAIN_ROOT_ACCOUNT_ID
@@ -57,18 +85,29 @@ const buildLaunchUrl = (messageUrl, placement, display) => {
const assignmentParam = assignmentId ? `&assignment_id=${assignmentId}` : ''
// xsslint safeString.property window.location
- toolLaunchUrl.searchParams.append('platform_redirect_url', window.location)
+ toolLaunchUrl.searchParams.append('platform_redirect_url', window.location.toString())
toolLaunchUrl.searchParams.append('full_win_launch_requested', '1')
const encodedToolLaunchUrl = encodeURIComponent(toolLaunchUrl.toString())
return `${baseUrl}&url=${encodedToolLaunchUrl}${clientIdParam}${placementParam}${assignmentParam}`
}
-const handler = ({message}) => {
+const handler: LtiMessageHandler<{
+ data: {
+ url: string
+ launchType: string
+ launchOptions: {
+ width?: number
+ height?: number
+ }
+ placement: string
+ display: string
+ }
+}> = ({message}) => {
const {url, launchType, launchOptions, placement, display} = parseData(message.data)
const launchUrl = buildLaunchUrl(url, placement, display)
- let proxy
+ let proxy: Window | null = null
switch (launchType) {
case 'popup': {
const width = launchOptions.width || 800
@@ -95,6 +134,7 @@ const handler = ({message}) => {
// keep a reference to close later
ltiState.fullWindowProxy = proxy
+ return false
}
export default handler
diff --git a/ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.js b/ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.ts
similarity index 85%
rename from ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.js
rename to ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.ts
index 0c179e70a88..b4844245b85 100644
--- a/ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.js
+++ b/ui/shared/lti/jquery/subjects/toggleCourseNavigationMenu.ts
@@ -17,7 +17,11 @@
*/
import {toggleCourseNav} from '@canvas/courses/jquery/toggleCourseNav'
+import {LtiMessageHandler} from '../lti_message_handler'
-const handler = () => toggleCourseNav()
+const handler: LtiMessageHandler = () => {
+ toggleCourseNav()
+ return false
+}
export default handler
diff --git a/ui/shared/lti/jquery/tool_launch_resizer.js b/ui/shared/lti/jquery/tool_launch_resizer.ts
similarity index 77%
rename from ui/shared/lti/jquery/tool_launch_resizer.js
rename to ui/shared/lti/jquery/tool_launch_resizer.ts
index 63fa6fc4871..cbe2dc10876 100644
--- a/ui/shared/lti/jquery/tool_launch_resizer.js
+++ b/ui/shared/lti/jquery/tool_launch_resizer.ts
@@ -19,15 +19,17 @@
import $ from 'jquery'
export default class ToolLaunchResizer {
- constructor(minToolHeight) {
+ minToolHeight: number
+
+ constructor(minToolHeight?: number) {
this.minToolHeight = minToolHeight || 450
}
- sanitizedWrapperId(wrapperId) {
+ sanitizedWrapperId(wrapperId?: string) {
return wrapperId?.toString()?.replace(/[^a-zA-Z0-9_-]/g, '')
}
- tool_content_wrapper(wrapperId) {
+ tool_content_wrapper(wrapperId?: string) {
let container = $(`div[data-tool-wrapper-id*='${this.sanitizedWrapperId(wrapperId)}']`)
const tool_content_wrapper = $('.tool_content_wrapper')
if (container.length <= 0 && tool_content_wrapper.length === 1) {
@@ -36,7 +38,15 @@ export default class ToolLaunchResizer {
return container
}
- resize_tool_content_wrapper(height, container, force_height = false) {
+ resize_tool_content_wrapper(
+ height: number | string,
+
+ // disabling b/c eslint fails, saying 'MessageEventSource' is not defined, but it's
+ // defined in lib.dom.d.ts
+ // eslint-disable-next-line no-undef
+ container: JQuery,
+ force_height = false
+ ) {
let setHeight = height
if (typeof setHeight !== 'number') {
setHeight = this.minToolHeight
diff --git a/ui/shared/lti/jquery/util.js b/ui/shared/lti/jquery/util.js
deleted file mode 100644
index 5d449ff5afe..00000000000
--- a/ui/shared/lti/jquery/util.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2021 - present Instructure, Inc.
- *
- * This file is part of Canvas.
- *
- * Canvas is free software: you can redistribute it and/or modify it under
- * the terms of the GNU Affero General Public License as published by the Free
- * Software Foundation, version 3 of the License.
- *
- * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
- * details.
- *
- * You should have received a copy of the GNU Affero General Public License along
- * with this program. If not, see .
- */
-
-let beforeUnloadHandler
-function setUnloadMessage(msg) {
- removeUnloadMessage()
-
- beforeUnloadHandler = function (e) {
- return (e.returnValue = msg || '')
- }
- window.addEventListener('beforeunload', beforeUnloadHandler)
-}
-
-function removeUnloadMessage() {
- if (beforeUnloadHandler) {
- window.removeEventListener('beforeunload', beforeUnloadHandler)
- beforeUnloadHandler = null
- }
-}
-
-function findDomForWindow(sourceWindow) {
- const iframes = document.getElementsByTagName('IFRAME')
- for (let i = 0; i < iframes.length; i += 1) {
- if (iframes[i].contentWindow === sourceWindow) {
- return iframes[i]
- }
- }
- return null
-}
-
-export {setUnloadMessage, removeUnloadMessage, findDomForWindow}
diff --git a/ui/shared/lti/jquery/util.ts b/ui/shared/lti/jquery/util.ts
new file mode 100644
index 00000000000..dd6e9d648e2
--- /dev/null
+++ b/ui/shared/lti/jquery/util.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+let beforeUnloadHandler: null | ((e: BeforeUnloadEvent) => void) = null
+
+export function setUnloadMessage(msg: string) {
+ removeUnloadMessage()
+
+ beforeUnloadHandler = function (e: BeforeUnloadEvent) {
+ return (e.returnValue = msg || '')
+ }
+ window.addEventListener('beforeunload', beforeUnloadHandler)
+}
+
+export function removeUnloadMessage() {
+ if (beforeUnloadHandler) {
+ window.removeEventListener('beforeunload', beforeUnloadHandler)
+ beforeUnloadHandler = null
+ }
+}
+
+// disabling b/c eslint fails, saying 'MessageEventSource' is not defined, but it's
+// defined in lib.dom.d.ts
+// eslint-disable-next-line no-undef
+export function findDomForWindow(sourceWindow?: MessageEventSource | null) {
+ const iframes = Array.from(document.getElementsByTagName('iframe'))
+ const iframe = iframes.find(iframe => iframe.contentWindow === sourceWindow)
+ return iframe || null
+}
+
+// https://stackoverflow.com/a/70029241
+// this can be removed and replace with `k in o`
+// when we upgrade to TS 4.9+
+export function hasKey(
+ k: K,
+ o: T
+): o is T & Record {
+ return k in o
+}
+
+export function getKey(key: string, o: unknown): unknown {
+ return typeof o === 'object' && o !== null && hasKey(key, o) ? o[key] : undefined
+}