feat: [FC-0070] add events and style for rendering Split xblock in chromeless template (#35813)

This feature introduces functionalities to improve XBlock interactions within iframes:

  * Add styles that adopt default styles for Split Test which renders chromless template via iframe in MFE Authoring.
  * When the isIframeEmbed option is enabled, the XBlock sends a postMessage to the parent window. When sending such a message, the standard link transition is cancelled and the transition is carried out in MFE Authoring.
This commit is contained in:
Ihor Romaniuk
2025-03-31 23:31:59 +02:00
committed by GitHub
parent e5cafb6cc0
commit f5c17bb88c
19 changed files with 500 additions and 172 deletions

View File

@@ -82,6 +82,22 @@ def is_unit(xblock, parent_xblock=None):
return False
def is_library_content(xblock):
"""
Returns true if the specified xblock is library content.
"""
return xblock.category == 'library_content'
def get_parent_if_split_test(xblock):
"""
Returns the parent of the specified xblock if it is a split test, otherwise returns None.
"""
parent_xblock = get_parent_xblock(xblock)
if parent_xblock and parent_xblock.category == 'split_test':
return parent_xblock
def xblock_has_own_studio_page(xblock, parent_xblock=None):
"""
Returns true if the specified xblock has an associated Studio page. Most xblocks do

View File

@@ -25,7 +25,11 @@ from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
from cms.djangoapps.contentstore.helpers import is_unit
from cms.djangoapps.contentstore.helpers import (
get_parent_if_split_test,
is_unit,
is_library_content,
)
from cms.djangoapps.contentstore.toggles import (
libraries_v1_enabled,
libraries_v2_enabled,
@@ -148,11 +152,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st
except ItemNotFoundError:
return HttpResponseBadRequest()
is_unit_page = is_unit(xblock)
unit = xblock if is_unit_page else None
if use_new_unit_page(course.id):
if is_unit(xblock) or is_library_content(xblock):
return redirect(get_unit_url(course.id, xblock.location))
if is_unit_page and use_new_unit_page(course.id):
return redirect(get_unit_url(course.id, unit.location))
if split_xblock := get_parent_if_split_test(xblock):
return redirect(get_unit_url(course.id, split_xblock.location))
container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
container_handler_context.update({

View File

@@ -611,6 +611,7 @@ def _create_block(request):
modulestore().update_item(created_block, request.user.id)
response["upstreamRef"] = upstream_ref
response["static_file_notices"] = asdict(static_file_notices)
response["parent_locator"] = parent_locator
return JsonResponse(response)

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path
d="M19.8 18.4 14 10.67V6.5l1.35-1.69c.26-.33.03-.81-.39-.81H9.04c-.42 0-.65.48-.39.81L10 6.5v4.17L4.2 18.4c-.49.66-.02 1.6.8 1.6h14c.82 0 1.29-.94.8-1.6z"
fill="currentColor">
</path>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path
d="M2.21 10.47 5 9.36 7.25 15H8V2h2.5v10h1V0H14v12h1V1.5h2.5V12h1V4.5H21V16c0 4.42-3.58 8-8 8-3.26 0-6.19-1.99-7.4-5.02l-3.39-8.51z"
fill="currentColor">
</path>
</svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path d="M21 3H3v18h18V3zM7.5 18c-.83 0-1.5-.67-1.5-1.5S6.67 15 7.5 15s1.5.67 1.5 1.5S8.33 18 7.5 18zm0-9C6.67 9 6 8.33 6 7.5S6.67 6 7.5 6 9 6.67 9 7.5 8.33 9 7.5 9zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm0-9c-.83 0-1.5-.67-1.5-1.5S15.67 6 16.5 6s1.5.67 1.5 1.5S17.33 9 16.5 9z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path d="M4 6H2v16h16v-2H4V6zm18-4H6v16h16V2zm-3 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960" fill="none" role="img" focusable="false" aria-hidden="true">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path
d="M3 10h11v2H3v-2zm0-2h11V6H3v2zm0 8h7v-2H3v2zm15.01-3.13 1.41-1.41 2.12 2.12-1.41 1.41-2.12-2.12zm-.71.71-5.3 5.3V21h2.12l5.3-5.3-2.12-2.12z"
fill="currentColor">
</path>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path
d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"
fill="currentColor">
</path>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
<path d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true">
<path d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -42,10 +42,38 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add
},
showComponentTemplates: function(event) {
var type;
var type, parentLocator, model, parentBlockType;
event.preventDefault();
event.stopPropagation();
type = $(event.currentTarget).data('type');
parentLocator = $(event.currentTarget).closest('.xblock[data-usage-id]').data('usage-id');
parentBlockType = $(event.currentTarget).parents('.xblock-author_view').last().data('block-type');
model = this.collection.models.find(function(item) { return item.type === type; }) || {};
try {
if (this.options.isIframeEmbed && parentBlockType !== 'split_test') {
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();
@@ -65,11 +93,25 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add
var self = this,
$element = $(event.currentTarget),
saveData = $element.data(),
oldOffset = ViewUtils.getScrollOffset(this.$el);
oldOffset = ViewUtils.getScrollOffset(this.$el),
usageId = $element.closest('.xblock[data-usage-id]').data('usage-id');
event.preventDefault();
this.closeNewComponent(event);
if (saveData.type === 'library_v2') {
try {
if (this.options.isIframeEmbed) {
return window.parent.postMessage(
{
type: 'showSingleComponentPicker',
payload: { usageId },
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
var modal = new AddLibraryContent();
modal.showComponentPicker(
this.options.libraryContentPickerUrl,

View File

@@ -40,6 +40,7 @@ function($, _, Backbone, gettext, BasePage,
'change .header-library-checkbox': 'toggleLibraryComponent',
'click .collapse-button': 'collapseXBlock',
'click .xblock-view-action-button': 'viewXBlockContent',
'click .xblock-view-group-link': 'viewXBlockContent',
},
options: {
@@ -60,8 +61,9 @@ function($, _, Backbone, gettext, BasePage,
initialize: function(options) {
BasePage.prototype.initialize.call(this, options);
this.viewClass = options.viewClass || this.defaultViewClass;
this.isLibraryPage = (this.model.attributes.category === 'library');
this.isLibraryContentPage = (this.model.attributes.category === 'library_content');
this.isLibraryPage = this.model.attributes.category === 'library';
this.isLibraryContentPage = this.model.attributes.category === 'library_content';
this.isSplitTestContentPage = this.model.attributes.category === 'split_test';
this.nameEditor = new XBlockStringFieldEditor({
el: this.$('.wrapper-xblock-field'),
model: this.model
@@ -131,13 +133,16 @@ function($, _, Backbone, gettext, BasePage,
if (this.options.isIframeEmbed) {
window.addEventListener('message', (event) => {
const { data } = event;
const { data: initialData } = event;
if (!data) return;
if (!initialData) return;
let xblockElement;
let xblockWrapper;
const data = { ...initialData };
data.payload = { ...data?.payload, ...data?.payload?.data };
if (data.payload && data.payload.locator) {
xblockElement = $(`[data-locator="${data.payload.locator}"]`);
xblockWrapper = $("li.studio-xblock-wrapper[data-locator='" + data.payload.locator + "']");
@@ -173,6 +178,25 @@ 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);
}
},
postMessageForHideProcessingNotification: function () {
this.postMessageToParent({
type: 'hideProcessingNotification',
message: 'Hide processing notification',
payload: {},
});
},
getViewParameters: function() {
return {
el: this.$('.wrapper-xblock'),
@@ -237,18 +261,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 +292,14 @@ function($, _, Backbone, gettext, BasePage,
renderAddXBlockComponents: function() {
var self = this;
if (self.options.canEdit && !self.options.isIframeEmbed) {
if (self.options.canEdit && (!self.options.isIframeEmbed || self.isSplitTestContentPage)) {
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 +309,7 @@ function($, _, Backbone, gettext, BasePage,
},
initializePasteButton() {
if (this.options.canEdit && !this.options.isIframeEmbed) {
if (this.options.canEdit && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) {
// We should have the user's clipboard status.
const data = this.options.clipboardData;
this.refreshPasteButton(data);
@@ -305,7 +326,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) {
if (!this.isLibraryPage && !this.isLibraryContentPage && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) {
this.postMessageForHideProcessingNotification();
// '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 +362,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 +391,9 @@ function($, _, Backbone, gettext, BasePage,
placeholderElement.remove();
});
}).done((data) => {
if (this.options.isIframeEmbed) {
this.postMessageForHideProcessingNotification();
}
const {
conflicting_files: conflictingFiles,
error_files: errorFiles,
@@ -646,17 +665,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 +681,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,18 +720,12 @@ function($, _, Backbone, gettext, BasePage,
},
openManageTags: function(event) {
const contentId = this.findXBlockElement(event.target).data('locator');
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'openManageTags',
payload: { contentId }
}, document.referrer
);
}
} catch (e) {
console.error(e);
const contentId = this.findXBlockElement(event.target).data('locator');
if (this.options.isIframeEmbed) {
this.postMessageToParent({
type: 'openManageTags',
payload: { contentId },
});
}
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
@@ -747,13 +742,17 @@ function($, _, Backbone, gettext, BasePage,
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
try {
if (this.options.isIframeEmbed) {
return window.parent.postMessage(
window.parent.postMessage(
{
type: 'copyXBlock',
type: this.isSplitTestContentPage ? 'copyXBlockLegacy' : 'copyXBlock',
message: 'Copy the XBlock',
payload: { usageId }
}, document.referrer
);
if (!this.isSplitTestContentPage) {
return;
}
}
} catch (e) {
console.error(e);
@@ -795,6 +794,7 @@ function($, _, Backbone, gettext, BasePage,
setTimeout(checkStatus, 1_000);
return deferred;
} else {
this.postMessageForHideProcessingNotification();
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
}
});
@@ -909,15 +909,12 @@ function($, _, Backbone, gettext, BasePage,
this.deleteComponent(this.findXBlockElement(event.target));
},
createPlaceholderElement: function() {
return $('<div/>', {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,
parentLocator = parentElement.data('locator'),
buttonPanel = target?.closest('.add-xblock-component'),
listPanel = buttonPanel?.prev(),
@@ -929,28 +926,55 @@ function($, _, Backbone, gettext, BasePage,
placeholderElement,
$container;
if (this.options.isIframeEmbed) {
if (this.options.isIframeEmbed && !this.isSplitTestContentPage) {
$container = $('ol.reorderable-container.ui-sortable');
scrollOffset = 0;
} else {
$container = listPanel;
scrollOffset = ViewUtils.getScrollOffset(buttonPanel);
if (!target.length && iframeMessageData.payload.parent_locator) {
$container = $('.xblock[data-usage-id="' + iframeMessageData.payload.parent_locator + '"]')
.find('ol.reorderable-container.ui-sortable');
}
if (!iframeMessageData) {
scrollOffset = ViewUtils.getScrollOffset(buttonPanel);
}
}
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 && iframeMessageData) {
if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') {
return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data);
}
}
if (this.options.isIframeEmbed && this.isSplitTestContentPage) {
this.postMessageToParent({
type: 'addNewComponent',
message: 'Add new XBlock',
payload: {},
});
if (iframeMessageData) {
return;
}
}
return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
.always(function () {
if (self.options.isIframeEmbed && self.isSplitTestContentPage) {
self.postMessageToParent({
type: 'hideProcessingNotification',
message: 'Hide processing notification',
payload: {}
});
return true;
}
})
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
});
});
},
duplicateComponent: function(xblockElement) {
@@ -966,17 +990,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 +1046,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) {
@@ -1065,19 +1082,16 @@ function($, _, Backbone, gettext, BasePage,
},
viewXBlockContent: function(event) {
try {
if (this.options.isIframeEmbed) {
event.preventDefault();
var usageId = event.currentTarget.href.split('/').pop() || '';
window.parent.postMessage({
type: 'handleViewXBlockContent',
message: 'View the content of the XBlock',
payload: { usageId },
}, document.referrer);
return true;
}
} catch (e) {
console.error(e);
if (this.options.isIframeEmbed) {
event.preventDefault();
const usageId = event.currentTarget.href.split('/').pop() || '';
const isViewGroupLink = event.currentTarget.classList.contains('xblock-view-group-link');
this.postMessageToParent({
type: isViewGroupLink ? 'handleViewGroupConfigurations' : 'handleViewXBlockContent',
message: isViewGroupLink ? 'View the group configurations page' : 'View the content of the XBlock',
payload: { usageId },
});
return true;
}
},
@@ -1142,6 +1156,17 @@ function($, _, Backbone, gettext, BasePage,
destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/' + blockType[1] + '/' + encodeURI(data.locator);
}
if (this.options.isIframeEmbed && this.isSplitTestContentPage) {
return this.postMessageToParent({
type: 'handleRedirectToXBlockEditPage',
message: 'Redirect to xBlock edit page',
payload: {
type: blockType[1],
locator: encodeURI(data.locator),
},
});
}
window.location.href = destinationUrl;
return;
}

View File

@@ -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: ($baseline * .3);
padding: ($baseline * .15) ($baseline / 2);
&:hover {
background-color: $primary;
color: $white;
}
}
}
&.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;
}
&.validation.has-warnings {
color: $black;
background-color: $xblock-message-warning-bg;
border-color: $xblock-message-warning-border-color;
border-top-width: 1px;
.icon {
color: $xblock-message-warning-border-color;
.action-button-text {
line-height: 20px;
}
}
a {
color: $primary;
}
}
.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);
&.level-page {
.xblock-message {
padding: ($baseline * .75) ($baseline * 1.2);
border-radius: 0 0 4px 4px;
.xblock-message-list {
color: $black;
}
&.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;
}
}
&.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;
}
}
&.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-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;
}
@@ -176,6 +219,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: ($baseline * 1.5) ($baseline / 2) 0;
}
}
.edit-xblock-modal select {
@@ -443,8 +493,8 @@ html {
}
&.xmodule_DoneXBlock {
margin-top: 60px;
padding: 0 20px;
margin-top: ($baseline * 3);
padding: 0 $baseline;
}
.xblock-actions {
@@ -578,7 +628,7 @@ html {
}
body [class*="view-"] .openassessment_editor_buttons.xblock-actions {
padding: 15px 2% 3px 2%;
padding: ($baseline * .75) 2% ($baseline * .15) 2%;
}
[class*="view-"] {
@@ -634,7 +684,7 @@ body [class*="view-"] .openassessment_editor_buttons.xblock-actions {
.list-input.settings-list {
.field.comp-setting-entry.is-set .setting-input {
color: $text-color;
margin-bottom: 5px;
margin-bottom: ($baseline * .25);
}
select {
@@ -733,7 +783,7 @@ select {
#openassessment_editor_header .editor_tabs .oa_editor_tab {
@extend %light-button;
padding: 0 10px;
padding: 0 ($baseline / 2);
}
#openassessment_editor_header,
@@ -762,7 +812,7 @@ select {
#oa_rubric_editor_wrapper .openassessment_criterion_option
.openassessment_criterion_option_point_wrapper label input {
min-width: 70px;
font-size: 18px;
font-size: $base-font-size;
height: 44px;
}
@@ -835,7 +885,7 @@ select {
width: 100%;
&.background-url {
margin-bottom: 10px;
margin-bottom: ($baseline / 2);
}
&.autozone-layout {
@@ -858,3 +908,104 @@ 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: 176px;
height: 110px;
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: 15.75px;
font-weight: 400;
}
.beta {
color: $white;
background-color: $primary;
padding: ($baseline * .1) ($baseline * .4) ($baseline * .2);
font-size: 13.5px;
font-weight: 700;
line-height: 1;
margin: -($baseline * .3) 0 0;
}
}
}
}
.new-component-templates {
border: 1px solid $border-color;
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15);
margin: $baseline;
overflow: hidden;
.button-component:hover {
background: $primary;
}
.cancel-button {
@extend %primary-button;
}
}
}
}
.paste-component {
margin: ($baseline * 1.2) ($baseline / 2) 0;
.paste-component-whats-in-clipboard .clipboard-details-popup {
right: ($baseline / 2 * -1);
}
.paste-component-button.button {
@extend %button-primary-outline;
}
}

View File

@@ -31,6 +31,8 @@
cursor: pointer;
background-image: none;
display: block;
box-shadow: none;
text-shadow: none;
&:hover {
background: darken($primary, 5%);
@@ -46,6 +48,35 @@
}
}
%button-primary-outline {
@extend %modal-actions-button;
color: $primary;
border-color: $primary;
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: 10px;
}
}
&:hover {
color: darken($primary, 10%);
background-color: lighten($primary, 80%);
border-color: darken($primary, 15%);
}
}
%light-button {
@extend %modal-actions-button;

View File

@@ -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"
);

View File

@@ -201,6 +201,7 @@ from openedx.core.release import RELEASE_LINE
outlineURL: "${outline_url | n, js_escaped_string}",
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
isIframeEmbed: true,
libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}",
}
);
</%static:webpack>

View File

@@ -17,7 +17,7 @@ show_link = group_configuration_url is not None
<p>
<span class="message-text">
${Text(_("This content experiment uses group configuration '{group_configuration_name}'.")).format(
group_configuration_name=Text(HTML("<a href='{}'>{}</a>")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name
group_configuration_name=Text(HTML("<a href='{}' class='xblock-view-group-link'>{}</a>")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name
)}
</span>
</p>