From 8e357d4072b6ef1780c77415310b038fd056bbff Mon Sep 17 00:00:00 2001 From: michaelroytman <mroytman@edx.org> Date: Thu, 15 Dec 2022 08:57:23 -0500 Subject: [PATCH] feat: replace PII consent modal with inline PII consent dialog and show for all LTI launch types This commit replaces the consent modal that appears before personally identifiable information (PII) is shared via an LTI launch with an inline consent dialog. The consent dialog better supports the three LTI launch types (i.e. inline, modal, and new_window). This commit also fixes a bug where the PII consent modal was not being displayed for inline or modal launches. --- lti_consumer/static/js/xblock_lti_consumer.js | 216 ++++++++++-------- lti_consumer/templates/html/student.html | 6 +- 2 files changed, 127 insertions(+), 95 deletions(-) diff --git a/lti_consumer/static/js/xblock_lti_consumer.js b/lti_consumer/static/js/xblock_lti_consumer.js index db835be..1a38d64 100644 --- a/lti_consumer/static/js/xblock_lti_consumer.js +++ b/lti_consumer/static/js/xblock_lti_consumer.js @@ -8,51 +8,51 @@ function LtiConsumerXBlock(runtime, element) { iframeModal: function (options) { var $trigger = $(this); var modal_id = $trigger.data("target"); - var defaults = {top: 100, overlay: 0.5, closeButton: null}; + var defaults = { top: 100, overlay: 0.5, closeButton: null }; var overlay_id = (modal_id + '_lean-overlay').replace('#', ''); var overlay = $("<div id='" + overlay_id + "' class='lean-overlay'></div>"); $("body").append(overlay); options = $.extend(defaults, options); return this.each(function () { var o = options; - $(this).click(function (e) { - var $modal = $(modal_id); - // If we are already in an iframe, skip creation of the modal, since - // it won't look good, anyway. Instead, we post a message to the parent - // window, requesting creation of a modal there. - // This is used by the courseware microfrontend. - if (window !== window.parent) { - window.parent.postMessage( - { - 'type': 'plugin.modal', - 'payload': { - 'url': window.location.origin + $modal.data('launch-url'), - 'title': $modal.find('iframe').attr('title'), - 'width': $modal.data('width') - } - }, - document.referrer - ); - return; - } - // Set iframe src attribute to launch LTI provider - $modal.find('iframe').attr('src', $modal.data('launch-url')); - $("#" + overlay_id).click(function () { - close_modal(modal_id) - }); - $(o.closeButton).click(function () { - close_modal(modal_id) - }); - var modal_height = $(modal_id).outerHeight(); - var modal_width = $(modal_id).outerWidth(); - $("#" + overlay_id).css({"display": "block", opacity: 0}); - $("#" + overlay_id).fadeTo(200, o.overlay); - $(modal_id).css({ - "display": "block" - }); - $(modal_id).fadeTo(200, 1); - $(modal_id).attr('aria-hidden', false); - $('body').css('overflow', 'hidden'); + + var $modal = $(modal_id); + // If we are already in an iframe, skip creation of the modal, since + // it won't look good, anyway. Instead, we post a message to the parent + // window, requesting creation of a modal there. + // This is used by the courseware microfrontend. + if (window !== window.parent) { + window.parent.postMessage( + { + 'type': 'plugin.modal', + 'payload': { + 'url': window.location.origin + $modal.data('launch-url'), + 'title': $modal.find('iframe').attr('title'), + 'width': $modal.data('width') + } + }, + document.referrer + ); + return; + } + // Set iframe src attribute to launch LTI provider + $modal.find('iframe').attr('src', $modal.data('launch-url')); + $("#" + overlay_id).click(function () { + close_modal(modal_id) + }); + $(o.closeButton).click(function () { + close_modal(modal_id) + }); + var modal_height = $(modal_id).outerHeight(); + var modal_width = $(modal_id).outerWidth(); + $("#" + overlay_id).css({ "display": "block", opacity: 0 }); + $("#" + overlay_id).fadeTo(200, o.overlay); + $(modal_id).css({ + "display": "block" + }); + $(modal_id).fadeTo(200, 1); + $(modal_id).attr('aria-hidden', false); + $('body').css('overflow', 'hidden'); e.preventDefault(); @@ -71,19 +71,19 @@ function LtiConsumerXBlock(runtime, element) { } }); - /* Redirect non-iframe tab to close button */ - var $inputs = $('select, input, textarea, button, a').filter(':visible').not(o.closeButton); - $inputs.on('focus', function(e) { - e.preventDefault(); - $(options.closeButton).focus(); - }); + /* Redirect non-iframe tab to close button */ + var $inputs = $('select, input, textarea, button, a').filter(':visible').not(o.closeButton); + $inputs.on('focus', function (e) { + e.preventDefault(); + $(options.closeButton).focus(); }); + }); function close_modal(modal_id) { var $modal = $(modal_id); $('select, input, textarea, button, a').off('focus'); $("#" + overlay_id).fadeOut(200); - $modal.css({"display": "none"}); + $modal.css({ "display": "none" }); $modal.attr('aria-hidden', true); $modal.find('iframe').attr('src', ''); $('body').css('overflow', 'auto'); @@ -92,68 +92,98 @@ function LtiConsumerXBlock(runtime, element) { } }); + function confirmDialog(message, triggerElement, showCancelButton) { + var def = $.Deferred(); + // Hide the button that triggered the event, i.e. the launch button. + triggerElement.hide(); + + $('<div id="dialog-container"></div>').insertAfter(triggerElement) // TODO: this will need some cute styling. It looks like trash but it works. + .append('<p>' + message + '</p>') + if (showCancelButton) { + $('#dialog-container') + .append('<button style="margin-right:1rem" id="cancel-button">Cancel</button>'); + } + $('#dialog-container').append('<button id="confirm-button">OK</button>'); + + // When a learner clicks "OK" or "Cancel" in the consent dialog, remove the consent dialog, show the launch + // button, and resolve the promise. + $('#confirm-button').click(function () { + // Show the button that triggered the event, i.e. the launch button. + triggerElement.show(); + $("#dialog-container").remove() + $('body').append('<h1>Confirm Dialog Result: <i>Yes</i></h1>'); + def.resolve("OK"); + }) + $('#cancel-button').click(function () { + // Hide the button that triggered the event, i.e. the launch button. + triggerElement.show() + $("#dialog-container").remove() + $('body').append('<h1>Confirm Dialog Result: <i>No</i></h1>'); + def.resolve("Cancel"); + }) + return def.promise(); + }; + var $element = $(element); var $ltiContainer = $element.find('.lti-consumer-container'); var askToSendUsername = $ltiContainer.data('ask-to-send-username') == 'True'; var askToSendEmail = $ltiContainer.data('ask-to-send-email') == 'True'; - // Apply click handler to modal launch button - $element.find('.btn-lti-modal').iframeModal({top: 200, closeButton: '.close-modal'}); - - // Apply click handler to new window launch button - $element.find('.btn-lti-new-window').click(function(){ - - // If this instance is configured to require username and/or email, ask user if it is okay to send them - // Do not launch if it is not okay - var destination = $(this).data('target') - - function confirmDialog(message) { - var def = $.Deferred(); - $('<div></div>').appendTo('body') // TODO: this will need some cute styling. It looks like trash but it works. - .html('<div><p>' + message + '</p></div>') - .dialog({ - modal: true, - title: 'Confirm', - zIndex: 10000, - autoOpen: true, - width: 'auto', - resizable: false, - dialogClass: 'confirm-dialog', - buttons: { - OK: function() { - $('body').append('<h1>Confirm Dialog Result: <i>Yes</i></h1>'); - def.resolve("OK"); - $(this).dialog("close"); - }, - Cancel: function() { - $('body').append('<h1>Confirm Dialog Result: <i>No</i></h1>'); - def.resolve("Cancel"); - $(this).dialog("close"); - } - }, - close: function(event, ui) { - $(this).remove(); - } - }).prev().css('background', 'white').css('color', '#000').css('border-color', 'transparent'); - return def.promise(); - }; - - if(askToSendUsername && askToSendEmail) { + function renderPIIConsentPromptIfRequired(onSuccess, showCancelButton=true) { + if (askToSendUsername && askToSendEmail) { msg = gettext("Click OK to have your username and e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."); } else if (askToSendUsername) { msg = gettext("Click OK to have your username sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."); } else if (askToSendEmail) { msg = gettext("Click OK to have your e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information."); } else { - window.open(destination); + onSuccess("OK"); + return; } - $.when(confirmDialog(msg)).then( - function(status) { + $.when(confirmDialog(msg, $(this), showCancelButton)).then(onSuccess); + } + + // Render consent dialog for inline elements immediately. + var $ltiIframeContainerElement = $element.find('#lti-iframe-container'); + $ltiIframeContainerElement.each(function () { + var ltiIframeTarget = $ltiIframeContainerElement.data('target') + renderPIIConsentPromptIfRequired.apply(this, [ + function (status) { + if (status === 'OK') { + // After getting consent to share PII, set the src attribute of the iframe to start the launch. + $ltiIframeContainerElement.find('iframe').attr('src', ltiIframeTarget); + } + }, + false + ]); + }) + + // Apply click handler to modal launch button. + var $ltiModalButton = $element.find('.btn-lti-modal'); + $ltiModalButton.click(function () { + renderPIIConsentPromptIfRequired.apply(this, [ + function (status) { + if (status === 'OK') { + $ltiModalButton.iframeModal({ + top: 200, closeButton: '.close-modal' + }) + } + } + ]); + }); + + // Apply click handler to new window launch button. + var $ltiNewWindowButton = $element.find('.btn-lti-new-window'); + $ltiNewWindowButton.click(function () { + renderPIIConsentPromptIfRequired.apply(this, [ + function (status) { if (status == "OK") { - window.open(destination); + window.open( + $ltiNewWindowButton.data('target') + ); } } - ); + ]); }); }); } diff --git a/lti_consumer/templates/html/student.html b/lti_consumer/templates/html/student.html index bd14c77..05afb6a 100644 --- a/lti_consumer/templates/html/student.html +++ b/lti_consumer/templates/html/student.html @@ -68,9 +68,11 @@ </section> % endif % if launch_target == 'iframe': - <div style="height:${inline_height}px;"> + <div id="lti-iframe-container" data-target="${form_url}" style="height:${inline_height}px;"> ## The result of the LTI launch form submit will be rendered here. - <%include file="templates/html/lti_iframe.html" args="initial_launch_url=form_url"/> + ## Don't pass in the initial_launch_url. Let the Javascript set the src, so we can get PII sharing consent + ## before the launch occurs, if needed. + <%include file="templates/html/lti_iframe.html" args="initial_launch_url=''"/> </div> % endif % elif not hide_launch: -- GitLab