diff --git a/cms/static/images/advanced-icon.svg b/cms/static/images/advanced-icon.svg new file mode 100644 index 000000000000..86096aefd236 --- /dev/null +++ b/cms/static/images/advanced-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/drag-and-drop-v2-icon.svg b/cms/static/images/drag-and-drop-v2-icon.svg new file mode 100644 index 000000000000..733fb744d7b3 --- /dev/null +++ b/cms/static/images/drag-and-drop-v2-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/itembank-icon.svg b/cms/static/images/itembank-icon.svg new file mode 100644 index 000000000000..b2198e1abac1 --- /dev/null +++ b/cms/static/images/itembank-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/library-icon.svg b/cms/static/images/library-icon.svg new file mode 100644 index 000000000000..a4fe81244e06 --- /dev/null +++ b/cms/static/images/library-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/library_v2-icon.svg b/cms/static/images/library_v2-icon.svg new file mode 100644 index 000000000000..4d69dbf9a063 --- /dev/null +++ b/cms/static/images/library_v2-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/openassessment-icon.svg b/cms/static/images/openassessment-icon.svg new file mode 100644 index 000000000000..4841b6b43dae --- /dev/null +++ b/cms/static/images/openassessment-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/problem-icon.svg b/cms/static/images/problem-icon.svg new file mode 100644 index 000000000000..7d51f436f7ee --- /dev/null +++ b/cms/static/images/problem-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/text-icon.svg b/cms/static/images/text-icon.svg new file mode 100644 index 000000000000..8588a471c954 --- /dev/null +++ b/cms/static/images/text-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/video-icon.svg b/cms/static/images/video-icon.svg new file mode 100644 index 000000000000..08f7444b621f --- /dev/null +++ b/cms/static/images/video-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/js/views/components/add_xblock.js b/cms/static/js/views/components/add_xblock.js index 29ce5eec7673..ea464fe45a36 100644 --- a/cms/static/js/views/components/add_xblock.js +++ b/cms/static/js/views/components/add_xblock.js @@ -42,10 +42,37 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add }, showComponentTemplates: function(event) { - var type; + var type, parentLocator, model; event.preventDefault(); event.stopPropagation(); + type = $(event.currentTarget).data('type'); + parentLocator = $(event.currentTarget).closest('.xblock[data-usage-id]').data('usage-id'); + model = this.collection.models.find(function(item) { return item.type === type; }) || {}; + + try { + if (this.options.isIframeEmbed) { + window.parent.postMessage( + { + type: 'showComponentTemplates', + payload: { + type: type, + parentLocator: parentLocator, + model: { + type: model.type, + display_name: model.display_name, + templates: model.templates, + support_legend: model.support_legend, + }, + } + }, document.referrer + ); + } + return true; + } catch (e) { + console.error(e); + } + this.$('.new-component').slideUp(250); this.$('.new-component-' + type).slideDown(250); this.$('.new-component-' + type + ' div').focus(); diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index dc44b15238ad..802b09717afe 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -173,6 +173,17 @@ function($, _, Backbone, gettext, BasePage, this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved); }, + postMessageToParent: function(body, callbackFn = null) { + try { + window.parent.postMessage(body, document.referrer); + if (callbackFn) { + callbackFn(); + } + } catch (e) { + console.error('Failed to post message:', e); + } + }, + getViewParameters: function() { return { el: this.$('.wrapper-xblock'), @@ -237,18 +248,14 @@ function($, _, Backbone, gettext, BasePage, const scrollOffset = scrollOffsetString ? parseInt(scrollOffsetString, 10) : 0; if (scrollOffset) { - try { - window.parent.postMessage( - { - type: 'scrollToXBlock', - message: 'Scroll to XBlock', - payload: { scrollOffset } - }, document.referrer - ); - localStorage.removeItem('modalEditLastYPosition'); - } catch (e) { - console.error(e); - } + self.postMessageToParent( + { + type: 'scrollToXBlock', + message: 'Scroll to XBlock', + payload: { scrollOffset } + }, + () => localStorage.removeItem('modalEditLastYPosition') + ); } } }, @@ -272,13 +279,15 @@ function($, _, Backbone, gettext, BasePage, renderAddXBlockComponents: function() { var self = this; - if (self.options.canEdit && !self.options.isIframeEmbed) { + var isSplitTest = self.model.attributes.category === 'split_test'; + if (self.options.canEdit && (!self.options.isIframeEmbed || isSplitTest)) { this.$('.add-xblock-component').each(function(index, element) { var component = new AddXBlockComponent({ el: element, createComponent: _.bind(self.createComponent, self), collection: self.options.templates, libraryContentPickerUrl: self.options.libraryContentPickerUrl, + isIframeEmbed: self.options.isIframeEmbed, }); component.render(); }); @@ -288,7 +297,8 @@ function($, _, Backbone, gettext, BasePage, }, initializePasteButton() { - if (this.options.canEdit && !this.options.isIframeEmbed) { + var isSplitTest = this.model.attributes.category === 'split_test'; + if (this.options.canEdit && (!this.options.isIframeEmbed || isSplitTest)) { // We should have the user's clipboard status. const data = this.options.clipboardData; this.refreshPasteButton(data); @@ -305,7 +315,8 @@ function($, _, Backbone, gettext, BasePage, refreshPasteButton(data) { // Do not perform any changes on paste button since they are not // rendered on Library or LibraryContent pages - if (!this.isLibraryPage && !this.isLibraryContentPage && !this.options.isIframeEmbed) { + var isSplitTest = this.model.attributes.category === 'split_test'; + if (!this.isLibraryPage && !this.isLibraryContentPage && (!this.options.isIframeEmbed || isSplitTest)) { // 'data' is the same data returned by the "get clipboard status" API endpoint // i.e. /api/content-staging/v1/clipboard/ if (this.options.canEdit && data.content) { @@ -340,17 +351,11 @@ function($, _, Backbone, gettext, BasePage, /** The user has clicked on the "Paste Component button" */ pasteComponent(event) { event.preventDefault(); - try { - if (this.options.isIframeEmbed) { - window.parent.postMessage( - { - type: 'pasteComponent', - payload: {} - }, document.referrer - ); - } - } catch (e) { - console.error(e); + if (this.options.isIframeEmbed) { + this.postMessageToParent({ + type: 'pasteComponent', + payload: {}, + }); } // Get the ID of the container (usually a unit/vertical) that we're pasting into: const parentElement = this.findXBlockElement(event.target); @@ -375,6 +380,13 @@ function($, _, Backbone, gettext, BasePage, placeholderElement.remove(); }); }).done((data) => { + if (this.options.isIframeEmbed) { + this.postMessageToParent({ + type: 'hideProcessingNotification', + message: 'Hide processing notification', + payload: {}, + }); + } const { conflicting_files: conflictingFiles, error_files: errorFiles, @@ -646,17 +658,11 @@ function($, _, Backbone, gettext, BasePage, subMenu.classList.toggle('is-shown'); if (!subMenu.classList.contains('is-shown') && this.options.isIframeEmbed) { - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { courseXBlockDropdownHeight: 0 } - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight: 0 } + }); } // Calculate the viewport height and the dropdown menu height. @@ -668,33 +674,21 @@ function($, _, Backbone, gettext, BasePage, if (courseUnitXBlockIframeHeight < courseXBlockDropdownHeight) { // If the dropdown menu is taller than the iframe, adjust the height of the dropdown menu. - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { courseXBlockDropdownHeight }, - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight }, + }); } else if ((courseXBlockDropdownHeight + clickYPosition) > courseUnitXBlockIframeHeight) { if (courseXBlockDropdownHeight > courseUnitXBlockIframeHeight / 2) { // If the dropdown menu is taller than half the iframe, send a message to adjust its height. - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { - courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2, - }, - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { + courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2, + }, + }); } else { // Move the dropdown menu upward to prevent it from overflowing out of the viewport. if (this.options.isIframeEmbed) { @@ -719,17 +713,11 @@ function($, _, Backbone, gettext, BasePage, }, openManageTags: function(event) { - try { - if (this.options.isIframeEmbed) { - window.parent.postMessage( - { - type: 'openManageTags', - payload: {} - }, document.referrer - ); - } - } catch (e) { - console.error(e); + if (this.options.isIframeEmbed) { + this.postMessageToParent({ + type: 'openManageTags', + payload: {}, + }); } const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); const contentId = this.findXBlockElement(event.target).data('locator'); @@ -909,15 +897,13 @@ function($, _, Backbone, gettext, BasePage, this.deleteComponent(this.findXBlockElement(event.target)); }, - createPlaceholderElement: function() { - return $('
', {class: 'studio-xblock-wrapper'}); - }, - createComponent: function(template, target, iframeMessageData) { // A placeholder element is created in the correct location for the new xblock // and then onNewXBlock will replace it with a rendering of the xblock. Note that // for xblocks that can't be replaced inline, the entire parent will be refreshed. var parentElement = this.findXBlockElement(target), + self = this, + isSplitTest = this.model.attributes.category === 'split_test', parentLocator = parentElement.data('locator'), buttonPanel = target?.closest('.add-xblock-component'), listPanel = buttonPanel?.prev(), @@ -929,7 +915,7 @@ function($, _, Backbone, gettext, BasePage, placeholderElement, $container; - if (this.options.isIframeEmbed) { + if (this.options.isIframeEmbed && !isSplitTest) { $container = $('ol.reorderable-container.ui-sortable'); scrollOffset = 0; } else { @@ -939,14 +925,31 @@ function($, _, Backbone, gettext, BasePage, placeholderElement = $placeholderEl.appendTo($container); - if (this.options.isIframeEmbed) { - if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') { - return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data); - } + if (this.options.isIframeEmbed && !isSplitTest) { + if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') { + return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data); + } + } + + if (this.options.isIframeEmbed && isSplitTest) { + this.postMessageToParent({ + type: 'addNewComponent', + message: 'Add new XBlock', + payload: {}, + }); } return $.postJSON(this.getURLRoot() + '/', requestData, _.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false)) + .success(function () { + if (self.options.isIframeEmbed && isSplitTest) { + self.postMessageToParent({ + type: 'hideProcessingNotification', + message: 'Hide processing notification', + payload: {} + }); + } + }) .fail(function() { // Remove the placeholder if the update failed placeholderElement.remove(); @@ -966,17 +969,11 @@ function($, _, Backbone, gettext, BasePage, placeholderElement = $placeholderEl.insertAfter(xblockElement); if (this.options.isIframeEmbed) { - try { - window.parent.postMessage( - { - type: 'scrollToXBlock', - message: 'Scroll to XBlock', - payload: { scrollOffset: xblockElement.height() } - }, document.referrer - ); - } catch (e) { - console.error(e); - } + this.postMessageToParent({ + type: 'scrollToXBlock', + message: 'Scroll to XBlock', + payload: { scrollOffset: xblockElement.height() } + }); const messageHandler = ({ data }) => { if (data && data.type === 'completeXBlockDuplicating') { @@ -1028,7 +1025,6 @@ function($, _, Backbone, gettext, BasePage, getSelectedLibraryComponents: function() { var self = this; var locator = this.$el.find('.studio-xblock-wrapper').data('locator'); - console.log(ModuleUtils); $.getJSON( ModuleUtils.getUpdateUrl(locator) + '/handler/get_block_ids', function(data) { diff --git a/cms/static/sass/course-unit-mfe-iframe-bundle.scss b/cms/static/sass/course-unit-mfe-iframe-bundle.scss index 10754b7a51b1..26dcb5fb2a10 100644 --- a/cms/static/sass/course-unit-mfe-iframe-bundle.scss +++ b/cms/static/sass/course-unit-mfe-iframe-bundle.scss @@ -8,6 +8,11 @@ html { } } +body, +#main { + background-color: transparent; +} + [class*="view-"] .wrapper { .inner-wrapper { max-width: 100%; @@ -39,67 +44,105 @@ html { .actions-list .action-item .action-button { border-radius: 4px; + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.3rem 1rem; &:hover { background-color: $primary; color: $white; } + + .action-button-text { + line-height: 2rem; + } } } - &.level-page .xblock-message { - padding: ($baseline * .75) ($baseline * 1.2); - border-radius: 0 0 4px 4px; + &.level-page { + .xblock-message { + padding: ($baseline * .75) ($baseline * 1.2); + border-radius: 0 0 4px 4px; - &.information { - color: $text-color; - background-color: $xblock-message-info-bg; - border-color: $xblock-message-info-border-color; - } + .xblock-message-list { + color: $black; + } - &.validation.has-warnings { - color: $black; - background-color: $xblock-message-warning-bg; - border-color: $xblock-message-warning-border-color; - border-top-width: 1px; + &.information, + &.validation.has-warnings, + &.validation.has-errors { + color: $black; + border-width: 0; + font-size: 16px; + line-height: 22px; + padding: ($baseline * 1.2); + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + } + + &.information { + background-color: $xblock-message-info-bg; + + .icon { + color: $xblock-message-info-icon-color; + } + } - .icon { - color: $xblock-message-warning-border-color; + &.validation.has-warnings { + background-color: $xblock-message-warning-bg; + + .icon { + color: $xblock-message-warning-icon-color; + } + } + + &.validation.has-errors { + background-color: $xblock-message-error-bg; + + .icon { + color: $xblock-message-error-icon-color; + } + } + + a { + color: $primary; + } } - } - a { - color: $primary; - } + &.studio-xblock-wrapper > .wrapper-xblock-message .xblock-message, + .xblock > .wrapper-xblock-message .xblock-message { + border-radius: 4px; + margin-bottom: ($baseline * 1.4); + } } - .xblock-author_view-library_content > .wrapper-xblock-message .xblock-message { - font-size: 16px; - line-height: 22px; - border-radius: 4px; - padding: ($baseline * 1.2); - box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); - margin-bottom: ($baseline * 1.4); + .xblock-author_view-split_test .wrapper-xblock { + background: $white; + box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); } &.level-element { box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); margin: 0 0 ($baseline * 1.4) 0; - } - &.level-element .xblock-header-primary { - background-color: $white; - } + .xblock-header-primary { + background-color: $white; + } - &.level-element .xblock-render { - background: $white; - margin: 0; - padding: $baseline; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; + .xblock-render { + background: $white; + margin: 0; + padding: $baseline; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } } .wrapper-xblock .header-actions .actions-list { + .wrapper-nav-sub { + z-index: 11; + } + .action-actions-menu:last-of-type .nav-sub { right: 120px; } @@ -171,6 +214,13 @@ html { } } } + + .wrapper-groups.is-inactive { + box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); + border-radius: 6px; + border: none; + margin: 3rem 1rem 0; + } } .edit-xblock-modal select { @@ -853,3 +903,112 @@ select { width: 100%; } } + +.xblock-render .add-xblock-component { + background: transparent; + padding: $baseline; + + .new-component { + h5 { + margin-bottom: ($baseline * 1.2); + font-size: 22px; + font-weight: 700; + color: $black; + } + + .new-component-type { + display: flex; + flex-wrap: wrap; + gap: ($baseline * .6); + align-items: center; + justify-content: center; + + .add-xblock-component-button { + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + width: 17.6rem; + height: 11rem; + color: $primary; + border-color: $primary; + background: transparent; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: ($baseline * .4); + + &:hover { + color: darken($primary, 10%); + background-color: lighten($primary, 80%); + border-color: darken($primary, 15%); + } + + .large-template-icon { + width: 24px; + height: 24px; + background: $primary; + + @each $name, $file in $template-icon-map { + &.large-#{$name}-icon { + mask: url("#{$static-path}/images/#{$file}.svg") center no-repeat; + } + } + } + + .name { + color: inherit; + font-size: 1.575rem; + font-weight: 400; + } + + .beta { + color: $white; + background-color: $primary; + padding: 0.2rem 0.8rem 0.4rem; + font-size: 1.35rem; + font-weight: 700; + line-height: 1; + margin: -0.6rem 0 0; + } + } + } + } +} + +.paste-component { + margin: 2.4rem 1rem 0; + + .paste-component-whats-in-clipboard .clipboard-details-popup { + right: -1rem; + } + + .paste-component-button.button { + padding: 9px 16px; + color: $primary; + border-color: $primary; + font-size: 1.8rem; + line-height: 1.33; + text-shadow: none; + font-weight: 400; + position: relative; + + &:focus { + color: $primary; + background: transparent; + + &:before { + content: ""; + position: absolute; + inset: -5px; + border: 2px solid $primary; + border-radius: 1rem; + } + } + + &:hover { + color: darken($primary, 10%); + background-color: lighten($primary, 80%); + border-color: darken($primary, 15%); + } + } +} diff --git a/cms/static/sass/partials/cms/theme/_variables-v1.scss b/cms/static/sass/partials/cms/theme/_variables-v1.scss index 0b3fe6b6e49b..b60bc15f2e8c 100644 --- a/cms/static/sass/partials/cms/theme/_variables-v1.scss +++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss @@ -317,6 +317,23 @@ $dark: #212529; $zindex-dropdown: 100; $xblock-message-info-bg: #eff8fa; -$xblock-message-info-border-color: #9cd2e6; +$xblock-message-info-icon-color: #9cd2e6; + $xblock-message-warning-bg: #fffdf0; -$xblock-message-warning-border-color: #fff6bf; +$xblock-message-warning-icon-color: #ffd900; + +$xblock-message-error-bg: #fbf2f3; +$xblock-message-error-icon-color: #c32d3a; + +$template-icon-map: ( + "library": "library-icon", + "library_v2": "library_v2-icon", + "itembank": "itembank-icon", + "advanced": "advanced-icon", + "html": "text-icon", + "openassessment": "openassessment-icon", + "problem": "problem-icon", + "video": "video-icon", + "drag-and-drop-v2": "drag-and-drop-v2-icon", + "text": "text-icon" +);