From b9dba671db45687294d5e8040dad81ecc9d88fff Mon Sep 17 00:00:00 2001 From: Xander Moffatt Date: Fri, 6 May 2022 10:53:44 -0600 Subject: [PATCH] allow lti postMessage on assignment show page why: * requested by studio, and it makes sense to respond to these messages in more places * possibly down the road monitor for messages on all pages instead of piecemeal * make sure only one message listener is active at a time closes INTEROP-7422 flag=none test plan: * edit an assignment and add a deep link to its description that points to the 1.3 test tool - make sure to include `{"width":400,"height":400}` in the iframe box so that it launches the tool in an iframe instead of a new window * save the assignment * open the browser dev tools console * in the assignment description, scroll to the bottom of the tool's iframe and send this postMessage: `{"subject":"lti.fetchWindowSize"}` * the console should show "message sent" and "message received" once * repeat the postMessage send while editing the assignment, should get the same result * turn on the assignments_2_student and assignments_2_teacher flags and make sure they are unlocked * repeat the postMessage while viewing the assignment, both as a student and as a teacher, should get one response message * as a teacher/admin, click into the description to edit it, and repeat the postMessage, should get one response message Change-Id: I5c8986d248cd1b9a344fba1060094aa97bfd7a86 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/291323 Tested-by: Service Cloud Jenkins QA-Review: Tucker Mcknight Product-Review: Alexis Nast Reviewed-by: Tucker Mcknight --- doc/api/lti_window_post_message.md | 97 +++++++++++-------- ui/features/assignment_edit/index.js | 2 + ui/features/assignment_show/index.js | 2 + ui/features/assignments_show_student/index.js | 6 +- ui/features/assignments_show_teacher/index.js | 2 + ui/shared/lti/jquery/messages.js | 10 +- 6 files changed, 76 insertions(+), 43 deletions(-) diff --git a/doc/api/lti_window_post_message.md b/doc/api/lti_window_post_message.md index 962fe0e1278..93bef543f9d 100644 --- a/doc/api/lti_window_post_message.md +++ b/doc/api/lti_window_post_message.md @@ -1,5 +1,4 @@ -Using window.postMessage in LTI Tools -===================================== +# Using window.postMessage in LTI Tools Canvas listens for events sent through the `window.postMessage` Javascript API (docs here) @@ -15,8 +14,10 @@ This token is present in the launch as a custom variable, `$com.instructure.Post should be passed in postMessage calls if it's present. If the LTI tool is launched in a iframe, as is most common, then postMessages should be sent to -`window.parent`. However, if the tool is launched in a new tab, window, or popup, then postMessages -should be directed to `window.opener`. The examples will use `window.parent`. +`window.top`. Usually `window.parent` should suffice, but there are some situations where that may +not refer to the Canvas window. `window.top` refers to the topmost parent window, which should always +be Canvas. However, if the tool is launched in a new tab, window, or popup, then postMessages +should be directed to `window.opener`. The examples will use `window.top`. # Message Types @@ -26,6 +27,7 @@ Launches the tool that sent the event in a full-window context (ie not inside a Mainly used for Safari launches, since Safari disables setting cookies inside iframes. **Required properties:** + - subject: "requestFullWindowLaunch" - data: either a string or an object - if a string, a url for relaunching the tool @@ -35,6 +37,7 @@ Mainly used for Safari launches, since Safari disables setting cookies inside if under the custom claim section (`https://www.instructure.com/placement`). **Optional properties:** + - data.launchType: defaults to "same_window" - "same_window": launches the tool in the same window, replacing Canvas entirely - "new_window": launches the tool in a new tab/window, which depends on user preference @@ -43,20 +46,20 @@ Mainly used for Safari launches, since Safari disables setting cookies inside if - data.launchOptions.height: for launchType: popup, defines the popup window's height. Defaults to 600. ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "requestFullWindowLaunch", + subject: 'requestFullWindowLaunch', data: { - url: "https://example-tool.com/launch", - placement: "course_navigation", - launchType: "new_window", + url: 'https://example-tool.com/launch', + placement: 'course_navigation', + launchType: 'new_window', launchOptions: { width: 1000, height: 800 } } }, - "*" + '*' ) ``` @@ -65,10 +68,11 @@ window.parent.postMessage( Opens and closes the course navigation sidebar, giving more space for the tool to display. **Required properties:** + - subject: "toggleCourseNavigationMenu" ```js -window.parent.postMessage({ subject: "toggleCourseNavigationMenu" }, "*") +window.top.postMessage({subject: 'toggleCourseNavigationMenu'}, '*') ``` ## lti.resourceImported @@ -78,10 +82,11 @@ Canvas will respond by reloading the page, if the tool was present in the extern apps tray. Used on wiki pages. **Required properties:** + - subject: "lti.resourceImported" ```js -window.parent.postMessage({ subject: "lti.resourceImported" }, "*") +window.top.postMessage({subject: 'lti.resourceImported'}, '*') ``` ## lti.hideRightSideWrapper @@ -89,14 +94,15 @@ window.parent.postMessage({ subject: "lti.resourceImported" }, "*") Tells Canvas to remove the right side nav in the assignments view. **Required properties:** + - subject: "lti.hideRightSideWrapper" ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.hideRightSideWrapper", + subject: 'lti.hideRightSideWrapper' }, - "*" + '*' ) ``` @@ -105,19 +111,21 @@ window.parent.postMessage( Tells Canvas to change the height of the iframe containing the tool. **Required properties:** + - subject: "lti.frameResize" - height: integer, in px **Optional properties:** + - token: postMessage token, discussed above. ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.frameResize", + subject: 'lti.frameResize', height: 400 }, - "*" + '*' ) ``` @@ -127,9 +135,11 @@ Sends a postMessage event back to the tool with details about the window size of the tool's containing iframe. **Required properties:** + - subject: "lti.fetchWindowSize" Returning postMessage includes the following properties: + - subject: "lti.fetchWindowSize.response" - height: height of the iframe - width: width of the iframe @@ -138,7 +148,7 @@ Returning postMessage includes the following properties: - scrollY: the number of px that the iframe is scrolled vertically ```js -window.parent.postMessage({ subject: "lti.fetchWindowSize" }, "*") +window.top.postMessage({subject: 'lti.fetchWindowSize'}, '*') ``` ## lti.showModuleNavigation @@ -146,16 +156,17 @@ window.parent.postMessage({ subject: "lti.fetchWindowSize" }, "*") Toggles the module navigation footer based on the message's content. **Required properties:** + - subject: "lti.showModuleNavigation" - show: Boolean, whether to show or hide the footer ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.frameResize", + subject: 'lti.frameResize', show: true }, - "*" + '*' ) ``` @@ -164,10 +175,11 @@ window.parent.postMessage( Scrolls the iframe all the way to the top of its container. **Required properties:** + - subject: "lti.scrollToTop" ```js -window.parent.postMessage({ subject: "lti.scrollToTop" }, "*") +window.top.postMessage({subject: 'lti.scrollToTop'}, '*') ``` ## lti.setUnloadMessage @@ -176,16 +188,17 @@ Sets a message to be shown in a browser dialog before page closes (ie "Do you really want to leave this page?") **Required properties:** + - subject: "lti.setUnloadMessage" - message: The message to be shown in the dialog ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.setUnloadMessage", - message: "Are you sure you want to leave this app?" + subject: 'lti.setUnloadMessage', + message: 'Are you sure you want to leave this app?' }, - "*" + '*' ) ``` @@ -194,10 +207,11 @@ window.parent.postMessage( Clears any set message to be shown on page close. Required properties + - subject: "lti.removeUnloadMessage" ```js -window.parent.postMessage({ subject: "lti.removeUnloadMessage" }, "*") +window.top.postMessage({subject: 'lti.removeUnloadMessage'}, '*') ``` ## lti.screenReaderAlert @@ -205,16 +219,17 @@ window.parent.postMessage({ subject: "lti.removeUnloadMessage" }, "*") Shows an alert for screen readers. **Required properties:** + - subject: "lti.screenReaderAlert" - body: The contents of the alert. ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.screenReaderAlert", - body: "An alert just for screen readers" + subject: 'lti.screenReaderAlert', + body: 'An alert just for screen readers' }, - "*" + '*' ) ``` @@ -224,23 +239,25 @@ Shows an alert using Canvas's alert system, and includes the name of the LTI tool that sent the message. **Required properties:** + - subject: "lti.showAlert" - body: The contents of the alert - can either be a string, or JSON string. **Optional properties:** + - alertType: "success", "warning", or "error". Defaults to "success". - title: A display name for the tool. If not provided, Canvas will attempt to supply the tool name or default to "External Tool". ```js -window.parent.postMessage( +window.top.postMessage( { - subject: "lti.showAlert", - alertType: "warning", - body: "An warning to be shown", - title: "Tool Name" + subject: 'lti.showAlert', + alertType: 'warning', + body: 'An warning to be shown', + title: 'Tool Name' }, - "*" + '*' ) ``` @@ -250,12 +267,14 @@ Sends a debounced postMessage event to the tool every time its containing iframe is scrolled. **Required properties:** + - subject: "lti.enableScrollEvents" Returning postMessage includes the following properties: + - subject: "lti.scroll" - scrollY: the number of px that the iframe is scrolled vertically ```js -window.parent.postMessage({ subject: "lti.enableScrollEvents" }, "*") +window.top.postMessage({subject: 'lti.enableScrollEvents'}, '*') ``` diff --git a/ui/features/assignment_edit/index.js b/ui/features/assignment_edit/index.js index fdcc0c8e881..b3868b5e81b 100644 --- a/ui/features/assignment_edit/index.js +++ b/ui/features/assignment_edit/index.js @@ -30,8 +30,10 @@ import GroupCategorySelector from '@canvas/groups/backbone/views/GroupCategorySe import PeerReviewsSelector from '@canvas/assignments/backbone/views/PeerReviewsSelector.coffee' import '@canvas/grading-standards' import LockManager from '@canvas/blueprint-courses/react/components/LockManager/index' +import {monitorLtiMessages} from '@canvas/lti/jquery/messages' ready(() => { + monitorLtiMessages() const lockManager = new LockManager() lockManager.init({itemType: 'assignment', page: 'edit'}) const lockedItems = lockManager.isChildContent() ? lockManager.getItemLocks() : {} diff --git a/ui/features/assignment_show/index.js b/ui/features/assignment_show/index.js index f2e3cd55759..9805f99ec92 100644 --- a/ui/features/assignment_show/index.js +++ b/ui/features/assignment_show/index.js @@ -38,6 +38,7 @@ import DirectShareUserModal from '@canvas/direct-sharing/react/components/Direct import DirectShareCourseTray from '@canvas/direct-sharing/react/components/DirectShareCourseTray' import {setupSubmitHandler} from '@canvas/assignments/jquery/reuploadSubmissionsHelper' import ready from '@instructure/ready' +import {monitorLtiMessages} from '@canvas/lti/jquery/messages' const I18n = useI18nScope('assignment') @@ -45,6 +46,7 @@ ready(() => { const lockManager = new LockManager() lockManager.init({itemType: 'assignment', page: 'show'}) renderCoursePacingNotice() + monitorLtiMessages() }) let studentGroupSelectionRequestTrackers = [] diff --git a/ui/features/assignments_show_student/index.js b/ui/features/assignments_show_student/index.js index e15c4e4ee13..de7497e4450 100644 --- a/ui/features/assignments_show_student/index.js +++ b/ui/features/assignments_show_student/index.js @@ -17,8 +17,10 @@ */ import renderAssignmentsApp from './react/index' -import $ from 'jquery' +import ready from '@instructure/ready' +import {monitorLtiMessages} from '@canvas/lti/jquery/messages' -$(() => { +ready(() => { + monitorLtiMessages() renderAssignmentsApp(ENV, $('
').appendTo('#content')[0]) }) diff --git a/ui/features/assignments_show_teacher/index.js b/ui/features/assignments_show_teacher/index.js index 256ca74ddc3..d348943c97c 100644 --- a/ui/features/assignments_show_teacher/index.js +++ b/ui/features/assignments_show_teacher/index.js @@ -18,8 +18,10 @@ import renderAssignmentsApp from './react/index' import ready from '@instructure/ready' +import {monitorLtiMessages} from '@canvas/lti/jquery/messages' ready(() => { + monitorLtiMessages() const elt = document.getElementById('content') renderAssignmentsApp(ENV, elt) }) diff --git a/ui/shared/lti/jquery/messages.js b/ui/shared/lti/jquery/messages.js index c853527db9b..92ac50d3693 100644 --- a/ui/shared/lti/jquery/messages.js +++ b/ui/shared/lti/jquery/messages.js @@ -114,11 +114,17 @@ async function ltiMessageHandler(e, platformStorageFeatureFlag = false) { } } +let hasListener = false + function monitorLtiMessages() { const platformStorageFeatureFlag = ENV?.FEATURES?.lti_platform_storage - window.addEventListener('message', e => { + const cb = e => { if (e.data !== '') ltiMessageHandler(e, platformStorageFeatureFlag) - }) + } + if (!hasListener) { + window.addEventListener('message', cb) + hasListener = true + } } export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}