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.
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
6
cms/static/images/advanced-icon.svg
Normal 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 |
6
cms/static/images/drag-and-drop-v2-icon.svg
Normal 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 |
3
cms/static/images/itembank-icon.svg
Normal 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 |
3
cms/static/images/library-icon.svg
Normal 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 |
3
cms/static/images/library_v2-icon.svg
Normal 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 |
6
cms/static/images/openassessment-icon.svg
Normal 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 |
6
cms/static/images/problem-icon.svg
Normal 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 |
3
cms/static/images/text-icon.svg
Normal 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 |
3
cms/static/images/video-icon.svg
Normal 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 |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||