When duplicating an xblock in the authoring MFE, the iframe was opening its own editor modal instead of the MFE modal.
1259 lines
58 KiB
JavaScript
1259 lines
58 KiB
JavaScript
/**
|
|
* XBlockContainerPage is used to display Studio's container page for an xblock which has children.
|
|
* This page allows the user to understand and manipulate the xblock and its children.
|
|
*/
|
|
define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page',
|
|
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
|
|
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
|
|
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
|
|
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
|
|
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
|
|
'js/views/utils/tagging_drawer_utils', 'js/utils/module', 'js/views/modals/preview_v2_library_changes',
|
|
'js/views/modals/select_v2_library_content'
|
|
],
|
|
function($, _, Backbone, gettext, BasePage,
|
|
ViewUtils, ContainerView, XBlockView,
|
|
AddXBlockComponent, EditXBlockModal, MoveXBlockModal,
|
|
XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
|
|
ContainerSubviews, UnitOutlineView, XBlockUtils,
|
|
NotificationView, PromptView, TaggingDrawerUtils, ModuleUtils,
|
|
PreviewLibraryChangesModal, SelectV2LibraryContent) {
|
|
'use strict';
|
|
|
|
var XBlockContainerPage = BasePage.extend({
|
|
// takes XBlockInfo as a model
|
|
|
|
events: {
|
|
'click .edit-button': 'editXBlock',
|
|
'click .access-button': 'editVisibilitySettings',
|
|
'click .duplicate-button': 'duplicateXBlock',
|
|
'click .copy-button': 'copyXBlock',
|
|
'click .move-button': 'showMoveXBlockModal',
|
|
'click .delete-button': 'deleteXBlock',
|
|
'click .library-sync-button': 'showXBlockLibraryChangesPreview',
|
|
'click .problem-bank-v2-add-button': 'showSelectV2LibraryContent',
|
|
'click .show-actions-menu-button': 'showXBlockActionsMenu',
|
|
'click .new-component-button': 'scrollToNewComponentButtons',
|
|
'click .save-button': 'saveSelectedLibraryComponents',
|
|
'click .paste-component-button': 'pasteComponent',
|
|
'click .manage-tags-button': 'openManageTags',
|
|
'change .header-library-checkbox': 'toggleLibraryComponent',
|
|
'click .collapse-button': 'collapseXBlock',
|
|
'click .xblock-view-action-button': 'viewXBlockContent',
|
|
'click .xblock-view-group-link': 'viewXBlockContent',
|
|
},
|
|
|
|
options: {
|
|
collapsedClass: 'is-collapsed',
|
|
canEdit: true, // If not specified, assume user has permission to make changes
|
|
clipboardData: { content: null },
|
|
},
|
|
|
|
view: 'container_preview',
|
|
|
|
defaultViewClass: ContainerView,
|
|
|
|
// Overridable by subclasses-- determines whether the XBlock component
|
|
// addition menu is added on initialization. You may set this to false
|
|
// if your subclass handles it.
|
|
components_on_init: true,
|
|
|
|
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.isSplitTestContentPage = this.model.attributes.category === 'split_test';
|
|
this.isVerticalContentPage = this.model.attributes.category === 'vertical';
|
|
|
|
this.nameEditor = new XBlockStringFieldEditor({
|
|
el: this.$('.wrapper-xblock-field'),
|
|
model: this.model
|
|
});
|
|
this.nameEditor.render();
|
|
if (!this.isLibraryPage) {
|
|
this.accessEditor = new XBlockAccessEditor({
|
|
el: this.$('.wrapper-xblock-field')
|
|
});
|
|
this.accessEditor.render();
|
|
}
|
|
if (this.options.action === 'new') {
|
|
this.nameEditor.$('.xblock-field-value-edit').click();
|
|
}
|
|
this.xblockView = this.getXBlockView();
|
|
this.messageView = new ContainerSubviews.MessageView({
|
|
el: this.$('.container-message'),
|
|
model: this.model
|
|
});
|
|
this.messageView.render();
|
|
this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
|
|
// Display access message on units and split test components
|
|
if (!this.isLibraryPage) {
|
|
this.containerAccessView = new ContainerSubviews.ContainerAccess({
|
|
el: this.$('.container-access'),
|
|
model: this.model
|
|
});
|
|
this.containerAccessView.render();
|
|
|
|
this.xblockPublisher = new ContainerSubviews.Publisher({
|
|
el: this.$('#publish-unit'),
|
|
model: this.model,
|
|
// When "Discard Changes" is clicked, the whole page must be re-rendered.
|
|
renderPage: this.render,
|
|
clipboardBroadcastChannel: this.clipboardBroadcastChannel,
|
|
});
|
|
this.xblockPublisher.render();
|
|
|
|
this.publishHistory = new ContainerSubviews.PublishHistory({
|
|
el: this.$('#publish-history'),
|
|
model: this.model
|
|
});
|
|
this.publishHistory.render();
|
|
|
|
this.viewLiveActions = new ContainerSubviews.ViewLiveButtonController({
|
|
el: this.$('.nav-actions'),
|
|
model: this.model
|
|
});
|
|
this.viewLiveActions.render();
|
|
|
|
if (!this.model.get('is_tagging_feature_disabled')) {
|
|
this.tagListView = new ContainerSubviews.TagList({
|
|
el: this.$('.unit-tags'),
|
|
model: this.model
|
|
});
|
|
this.tagListView.setupMessageListener();
|
|
this.tagListView.render();
|
|
}
|
|
|
|
this.unitOutlineView = new UnitOutlineView({
|
|
el: this.$('.wrapper-unit-overview'),
|
|
model: this.model
|
|
});
|
|
this.unitOutlineView.render();
|
|
|
|
}
|
|
|
|
if (this.options.isIframeEmbed) {
|
|
window.addEventListener('message', (event) => {
|
|
const { data: initialData } = event;
|
|
|
|
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 + "']");
|
|
} else {
|
|
xblockWrapper = $();
|
|
}
|
|
|
|
switch (data.type) {
|
|
case 'refreshXBlock':
|
|
this.render();
|
|
break;
|
|
case 'completeXBlockEditing':
|
|
this.refreshXBlock(xblockElement, false);
|
|
break;
|
|
case 'completeManageXBlockAccess':
|
|
this.refreshXBlock(xblockElement, false);
|
|
break;
|
|
case 'completeXBlockDuplicating':
|
|
this.refreshXBlock(xblockElement, true, true);
|
|
break;
|
|
case 'completeXBlockMoving':
|
|
xblockWrapper.hide();
|
|
break;
|
|
case 'rollbackMovedXBlock':
|
|
xblockWrapper.show();
|
|
break;
|
|
case 'addXBlock':
|
|
this.createComponent(this, xblockElement, data);
|
|
break;
|
|
case 'scrollToXBlock':
|
|
document.getElementById(data.payload.locator)?.scrollIntoView({behavior: "smooth"});
|
|
break;
|
|
default:
|
|
console.warn('Unhandled message type:', data.type);
|
|
}
|
|
});
|
|
}
|
|
|
|
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'),
|
|
model: this.model,
|
|
view: this.view
|
|
};
|
|
},
|
|
|
|
getXBlockView: function() {
|
|
return new this.viewClass(this.getViewParameters());
|
|
},
|
|
|
|
render: function(options) {
|
|
var self = this,
|
|
xblockView = this.xblockView,
|
|
loadingElement = this.$('.ui-loading'),
|
|
unitLocationTree = this.$('.unit-location'),
|
|
unitTags = this.$('.unit-tags'),
|
|
hiddenCss = 'is-hidden';
|
|
|
|
loadingElement.removeClass(hiddenCss);
|
|
|
|
// Hide both blocks until we know which one to show
|
|
xblockView.$el.addClass(hiddenCss);
|
|
|
|
// Render the xblock
|
|
xblockView.render({
|
|
done: function() {
|
|
// Show the xblock and hide the loading indicator
|
|
xblockView.$el.removeClass(hiddenCss);
|
|
loadingElement.addClass(hiddenCss);
|
|
|
|
// Notify the runtime that the page has been successfully shown
|
|
xblockView.notifyRuntime('page-shown', self);
|
|
|
|
if (self.components_on_init) {
|
|
// Render the add buttons. Paged containers should do this on their own.
|
|
self.renderAddXBlockComponents();
|
|
}
|
|
|
|
// Refresh the views now that the xblock is visible
|
|
self.onXBlockRefresh(xblockView);
|
|
unitLocationTree.removeClass(hiddenCss);
|
|
unitTags.removeClass(hiddenCss);
|
|
|
|
// Re-enable Backbone events for any updated DOM elements
|
|
self.delegateEvents();
|
|
|
|
// Show/hide the paste button
|
|
if (!self.isLibraryPage && !self.isLibraryContentPage) {
|
|
self.initializePasteButton();
|
|
}
|
|
|
|
var targetId = window.location.hash.slice(1);
|
|
if (targetId) {
|
|
var target = document.getElementById(targetId);
|
|
target.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
|
}
|
|
|
|
if (self.options.isIframeEmbed) {
|
|
const scrollOffsetString = localStorage.getItem('modalEditLastYPosition');
|
|
const scrollOffset = scrollOffsetString ? parseInt(scrollOffsetString, 10) : 0;
|
|
|
|
if (scrollOffset) {
|
|
self.postMessageToParent(
|
|
{
|
|
type: 'scrollToXBlock',
|
|
message: 'Scroll to XBlock',
|
|
payload: { scrollOffset }
|
|
},
|
|
() => localStorage.removeItem('modalEditLastYPosition')
|
|
);
|
|
}
|
|
}
|
|
},
|
|
block_added: options && options.block_added
|
|
});
|
|
},
|
|
|
|
findXBlockElement: function(target) {
|
|
return $(target).closest('.studio-xblock-wrapper');
|
|
},
|
|
|
|
getURLRoot: function() {
|
|
return this.xblockView.model.urlRoot;
|
|
},
|
|
|
|
onXBlockRefresh: function(xblockView, block_added, is_duplicate) {
|
|
this.xblockView.refresh(xblockView, block_added, is_duplicate);
|
|
// Update publish and last modified information from the server.
|
|
this.model.fetch();
|
|
},
|
|
|
|
renderAddXBlockComponents: function() {
|
|
var self = this;
|
|
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();
|
|
});
|
|
} else {
|
|
this.$('.add-xblock-component').remove();
|
|
}
|
|
},
|
|
|
|
initializePasteButton() {
|
|
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);
|
|
// Refresh the status when something is copied on another tab:
|
|
this.clipboardBroadcastChannel.onmessage = (event) => { this.refreshPasteButton(event.data); };
|
|
} else {
|
|
this.$(".paste-component").hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Given the latest information about the user's clipboard, hide or show the Paste button as appropriate.
|
|
*/
|
|
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 || 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) {
|
|
if (["vertical", "sequential", "chapter", "course"].includes(data.content.block_type)) {
|
|
// This is not suitable for pasting into a unit.
|
|
this.$(".paste-component").hide();
|
|
} else if (data.content.status === "expired") {
|
|
// This has expired and can no longer be pasted.
|
|
this.$(".paste-component").hide();
|
|
} else {
|
|
// The thing in the clipboard can be pasted into this unit:
|
|
const detailsPopupEl = this.$(".clipboard-details-popup")[0];
|
|
if (!detailsPopupEl) return; // This happens on the Problem Bank container page - no paste button is there anyways
|
|
detailsPopupEl.querySelector(".detail-block-name").innerText = data.content.display_name;
|
|
detailsPopupEl.querySelector(".detail-block-type").innerText = data.content.block_type_display;
|
|
detailsPopupEl.querySelector(".detail-course-name").innerText = data.source_context_title;
|
|
if (data.source_edit_url) {
|
|
detailsPopupEl.setAttribute("href", data.source_edit_url);
|
|
detailsPopupEl.classList.remove("no-edit-link");
|
|
} else {
|
|
detailsPopupEl.setAttribute("href", "#");
|
|
detailsPopupEl.classList.add("no-edit-link");
|
|
}
|
|
this.$(".paste-component").show();
|
|
}
|
|
} else {
|
|
this.$(".paste-component").hide();
|
|
}
|
|
}
|
|
},
|
|
|
|
/** The user has clicked on the "Paste Component button" */
|
|
pasteComponent(event) {
|
|
event.preventDefault();
|
|
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);
|
|
const parentLocator = parentElement.data('locator');
|
|
// Create a placeholder XBlock while we're pasting:
|
|
const $placeholderEl = $(this.createPlaceholderElement());
|
|
const addComponentsPanel = $(event.target).closest('.paste-component').prev();
|
|
const listPanel = addComponentsPanel.prev();
|
|
const scrollOffset = ViewUtils.getScrollOffset(addComponentsPanel);
|
|
const placeholderElement = $placeholderEl.appendTo(listPanel);
|
|
|
|
// Start showing a "Pasting" notification:
|
|
ViewUtils.runOperationShowingMessage(gettext('Pasting'), () => {
|
|
return $.postJSON(this.getURLRoot() + '/', {
|
|
parent_locator: parentLocator,
|
|
staged_content: "clipboard",
|
|
}).then((data) => {
|
|
this.onNewXBlock(placeholderElement, scrollOffset, false, data);
|
|
return data;
|
|
}).fail(() => {
|
|
// Remove the placeholder if the paste failed
|
|
placeholderElement.remove();
|
|
});
|
|
}).done((data) => {
|
|
if (this.options.isIframeEmbed) {
|
|
this.postMessageForHideProcessingNotification();
|
|
}
|
|
const {
|
|
conflicting_files: conflictingFiles,
|
|
error_files: errorFiles,
|
|
new_files: newFiles,
|
|
} = data.static_file_notices;
|
|
|
|
const notices = [];
|
|
if (errorFiles.length) {
|
|
notices.push((next) => new PromptView.Error({
|
|
title: gettext("Some errors occurred"),
|
|
message: (
|
|
gettext("The following required files could not be added to the course:") +
|
|
" " + errorFiles.join(", ")
|
|
),
|
|
actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}},
|
|
}));
|
|
}
|
|
if (conflictingFiles.length) {
|
|
notices.push((next) => new PromptView.Warning({
|
|
title: gettext("You may need to update a file(s) manually"),
|
|
message: (
|
|
gettext(
|
|
"The following files already exist in this course but don't match the " +
|
|
"version used by the component you pasted:"
|
|
) + " " + conflictingFiles.join(", ")
|
|
),
|
|
actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}},
|
|
}));
|
|
}
|
|
if (newFiles.length) {
|
|
notices.push(() => new NotificationView.Info({
|
|
title: gettext("New file(s) added to Files & Uploads."),
|
|
message: (
|
|
gettext("The following required files were imported to this course:") +
|
|
" " + newFiles.join(", ")
|
|
),
|
|
actions: {
|
|
primary: {
|
|
text: gettext('View files'),
|
|
click: function(notification) {
|
|
const section = document.querySelector('[data-course-assets]');
|
|
const assetsUrl = $(section).attr('data-course-assets');
|
|
window.location.href = assetsUrl;
|
|
return;
|
|
}
|
|
},
|
|
secondary: {
|
|
text: gettext('Dismiss'),
|
|
click: function(notification) {
|
|
return notification.hide();
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
if (notices.length) {
|
|
// Show the notices, one at a time:
|
|
const showNext = () => {
|
|
const view = notices.shift()(showNext);
|
|
view.show();
|
|
}
|
|
// Delay to avoid conflict with the "Pasting..." notification.
|
|
setTimeout(showNext, 1250);
|
|
}
|
|
});
|
|
},
|
|
|
|
editXBlock: function(event, options) {
|
|
event.preventDefault();
|
|
const isAccessButton = event.currentTarget.className === 'access-button';
|
|
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
|
|
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
|
|
|
|
try {
|
|
if (this.options.isIframeEmbed && isAccessButton) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Adjust the height of the dropdown menu',
|
|
payload: { courseXBlockDropdownHeight: 0 }
|
|
}, document.referrer
|
|
);
|
|
return window.parent.postMessage(
|
|
{
|
|
type: 'manageXBlockAccess',
|
|
message: 'Open the manage access modal',
|
|
payload: { usageId }
|
|
}, document.referrer
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
if (!options || options.view !== 'visibility_view') {
|
|
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
|
|
|
|
var useNewTextEditor = primaryHeader.attr('use-new-editor-text'),
|
|
useNewVideoEditor = primaryHeader.attr('use-new-editor-video'),
|
|
useNewProblemEditor = primaryHeader.attr('use-new-editor-problem'),
|
|
blockType = primaryHeader.attr('data-block-type');
|
|
|
|
if((useNewTextEditor === 'True' && blockType === 'html')
|
|
|| (useNewVideoEditor === 'True' && blockType === 'video')
|
|
|| (useNewProblemEditor === 'True' && blockType === 'problem')
|
|
) {
|
|
var destinationUrl = primaryHeader.attr('authoring_MFE_base_url')
|
|
+ '/' + blockType
|
|
+ '/' + encodeURI(primaryHeader.attr('data-usage-id'));
|
|
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
localStorage.setItem('modalEditLastYPosition', event.clientY.toString());
|
|
return window.parent.postMessage(
|
|
{
|
|
type: 'newXBlockEditor',
|
|
message: 'Open the new XBlock editor',
|
|
payload: {
|
|
blockType,
|
|
usageId: encodeURI(primaryHeader.attr('data-usage-id')),
|
|
}
|
|
}, document.referrer
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
var upstreamRef = primaryHeader.attr('data-upstream-ref');
|
|
if(upstreamRef) {
|
|
destinationUrl += '?upstreamLibRef=' + upstreamRef;
|
|
}
|
|
window.location.href = destinationUrl;
|
|
return;
|
|
}
|
|
|
|
if (this.options.isIframeEmbed) {
|
|
return window.parent.postMessage(
|
|
{
|
|
type: 'editXBlock',
|
|
message: 'Sends a message when the legacy modal window is shown',
|
|
payload: {
|
|
id: this.findXBlockElement(event.target).data('locator')
|
|
}
|
|
}, document.referrer
|
|
);
|
|
}
|
|
}
|
|
|
|
var xblockElement = this.findXBlockElement(event.target),
|
|
self = this,
|
|
modal = new EditXBlockModal(options);
|
|
|
|
modal.edit(xblockElement, this.model, {
|
|
readOnlyView: !this.options.canEdit,
|
|
refresh: function() {
|
|
self.refreshXBlock(xblockElement, false);
|
|
}
|
|
});
|
|
},
|
|
|
|
/** Show the modal for previewing changes before syncing a library-sourced XBlock. */
|
|
showXBlockLibraryChangesPreview: function(event, options) {
|
|
const xblockElement = this.findXBlockElement(event.target);
|
|
const self = this;
|
|
const xblockInfo = XBlockUtils.findXBlockInfo(xblockElement, this.model);
|
|
const courseAuthoringMfeUrl = this.model.attributes.course_authoring_url;
|
|
const headerElement = xblockElement.find('.xblock-header-primary');
|
|
const upstreamBlockId = headerElement.data('upstream-ref');
|
|
const upstreamBlockVersionSynced = headerElement.data('version-synced');
|
|
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'showXBlockLibraryChangesPreview',
|
|
payload: {
|
|
downstreamBlockId: xblockInfo.get('id'),
|
|
displayName: xblockInfo.get('display_name'),
|
|
isVertical: xblockInfo.isVertical(),
|
|
upstreamBlockId,
|
|
upstreamBlockVersionSynced,
|
|
}
|
|
}, document.referrer
|
|
);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
event.preventDefault();
|
|
var modal = new PreviewLibraryChangesModal(options);
|
|
modal.showPreviewFor(
|
|
xblockInfo,
|
|
courseAuthoringMfeUrl,
|
|
upstreamBlockId,
|
|
upstreamBlockVersionSynced,
|
|
function() { self.refreshXBlock(xblockElement, false); }
|
|
);
|
|
},
|
|
|
|
/** Show the multi-select library content picker, for adding to a Problem Bank (itembank) Component */
|
|
showSelectV2LibraryContent: function(event, options) {
|
|
event.preventDefault();
|
|
|
|
const xblockElement = this.findXBlockElement(event.target);
|
|
const modal = new SelectV2LibraryContent(options);
|
|
const courseAuthoringMfeUrl = this.model.attributes.course_authoring_url;
|
|
const itemBankBlockId = xblockElement.data("locator");
|
|
const pickerUrl = courseAuthoringMfeUrl + '/component-picker/multiple?variant=published';
|
|
|
|
modal.showComponentPicker(pickerUrl, (selectedBlocks) => {
|
|
// selectedBlocks has type: {usageKey: string, blockType: string}[]
|
|
let doneAddingAllBlocks = () => { this.refreshXBlock(xblockElement, false); };
|
|
let doneAddingBlock = () => {};
|
|
if (this.model.id === itemBankBlockId) {
|
|
// We're on the detailed view, showing all the components inside the problem bank.
|
|
// Create a placeholder that will become the new block(s)
|
|
const $insertSpot = xblockElement.find('.insert-new-lib-blocks-here');
|
|
doneAddingBlock = (addResult) => {
|
|
const $placeholderEl = $(this.createPlaceholderElement());
|
|
const placeholderElement = $placeholderEl.insertBefore($insertSpot);
|
|
placeholderElement.data('locator', addResult.locator);
|
|
return this.refreshXBlock(placeholderElement, true);
|
|
};
|
|
doneAddingAllBlocks = () => {};
|
|
}
|
|
// Note: adding all the XBlocks in parallel will cause a race condition 😢 so we have to add
|
|
// them one at a time:
|
|
let lastAdded = $.when();
|
|
for (const { usageKey, blockType } of selectedBlocks) {
|
|
const addData = {
|
|
library_content_key: usageKey,
|
|
category: blockType,
|
|
parent_locator: itemBankBlockId,
|
|
};
|
|
lastAdded = lastAdded.then(() => (
|
|
$.postJSON(this.getURLRoot() + '/', addData, doneAddingBlock)
|
|
));
|
|
}
|
|
// Now we actually add the block:
|
|
ViewUtils.runOperationShowingMessage(gettext('Adding'), () => {
|
|
return lastAdded.done(() => { doneAddingAllBlocks() });
|
|
});
|
|
}, this.options.isIframeEmbed);
|
|
},
|
|
|
|
/**
|
|
* If the new "Actions" menu is enabled, most XBlock actions like
|
|
* Duplicate, Move, Delete, Manage Access, etc. are moved into this
|
|
* menu. For this event, we just toggle displaying the menu.
|
|
* @param {*} event
|
|
*/
|
|
showXBlockActionsMenu: function(event) {
|
|
const showActionsButton = event.currentTarget;
|
|
const subMenu = showActionsButton.parentElement.querySelector('.wrapper-nav-sub');
|
|
|
|
// Close all open dropdowns
|
|
const elements = document.querySelectorAll("li.action-item.action-actions-menu.nav-item");
|
|
elements.forEach(element => {
|
|
if (element !== showActionsButton.parentElement) {
|
|
element.querySelector('.wrapper-nav-sub').classList.remove('is-shown');
|
|
}
|
|
});
|
|
|
|
// Code in 'base.js' normally handles toggling these dropdowns but since this one is
|
|
// not present yet during the domReady event, we have to handle displaying it ourselves.
|
|
subMenu.classList.toggle('is-shown');
|
|
|
|
if (!subMenu.classList.contains('is-shown') && this.options.isIframeEmbed) {
|
|
this.postMessageToParent({
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Adjust the height of the dropdown menu',
|
|
payload: { courseXBlockDropdownHeight: 0 }
|
|
});
|
|
}
|
|
|
|
// Calculate the viewport height and the dropdown menu height.
|
|
// Check if the dropdown would overflow beyond the iframe height based on the user's click position.
|
|
// If the dropdown overflows, adjust its position to display above the click point.
|
|
const iframeHeight = window.innerHeight;
|
|
const dropdownHeight = subMenu.offsetHeight;
|
|
const offsetBuffer = 10;
|
|
|
|
const targetRect = event.target.getBoundingClientRect();
|
|
const targetBottom = targetRect.bottom;
|
|
const targetTop = targetRect.top;
|
|
|
|
// Calculate total space needed below the target to fit dropdown
|
|
const dropdownBottom = targetBottom + dropdownHeight + offsetBuffer;
|
|
|
|
const dropdownFitsBelow = dropdownBottom <= iframeHeight;
|
|
const dropdownFitsAbove = dropdownHeight + offsetBuffer < targetTop;
|
|
|
|
if (!dropdownFitsBelow) {
|
|
if (dropdownFitsAbove && this.options.isIframeEmbed) {
|
|
// Display the dropdown above the button
|
|
subMenu.style.top = `-${dropdownHeight}px`;
|
|
} else {
|
|
// Request parent to expand iframe height to fit dropdown
|
|
const requiredExtraHeight = dropdownBottom - iframeHeight;
|
|
this.postMessageToParent({
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Expand iframe to fit dropdown',
|
|
payload: {
|
|
courseXBlockDropdownHeight: requiredExtraHeight,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// if propagation is not stopped, the event will bubble up to the
|
|
// body element, which will close the dropdown.
|
|
event.stopPropagation();
|
|
},
|
|
|
|
editVisibilitySettings: function(event) {
|
|
this.editXBlock(event, {
|
|
view: 'visibility_view',
|
|
// Translators: "title" is the name of the current component or unit being edited.
|
|
titleFormat: gettext('Editing access for: {title}'),
|
|
viewSpecificClasses: '',
|
|
modalSize: 'med'
|
|
});
|
|
},
|
|
|
|
openManageTags: function(event) {
|
|
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');
|
|
|
|
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
|
|
},
|
|
|
|
createPlaceholderElement: function() {
|
|
return $('<div/>', {class: 'studio-xblock-wrapper'});
|
|
},
|
|
|
|
copyXBlock: function(event) {
|
|
event.preventDefault();
|
|
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
|
|
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: this.isSplitTestContentPage ? 'copyXBlockLegacy' : 'copyXBlock',
|
|
message: 'Copy the XBlock',
|
|
payload: { usageId }
|
|
}, document.referrer
|
|
);
|
|
|
|
if (!this.isSplitTestContentPage) {
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
const clipboardEndpoint = "/api/content-staging/v1/clipboard/";
|
|
const element = this.findXBlockElement(event.target);
|
|
const usageKeyToCopy = element.data('locator');
|
|
// Start showing a "Copying" notification:
|
|
ViewUtils.runOperationShowingMessage(gettext('Copying'), () => {
|
|
return $.postJSON(
|
|
clipboardEndpoint,
|
|
{ usage_key: usageKeyToCopy },
|
|
).then((data) => {
|
|
const status = data.content?.status;
|
|
if (status === "ready") {
|
|
// The XBlock has been copied and is ready to use.
|
|
this.refreshPasteButton(data); // Update our UI
|
|
this.clipboardBroadcastChannel.postMessage(data); // And notify any other open tabs
|
|
return data;
|
|
} else if (status === "loading") {
|
|
// The clipboard is being loaded asynchonously.
|
|
// Poll the endpoint until the copying process is complete:
|
|
const deferred = $.Deferred();
|
|
const checkStatus = () => {
|
|
$.getJSON(clipboardEndpoint, (pollData) => {
|
|
const newStatus = pollData.content?.status;
|
|
if (newStatus === "ready") {
|
|
this.refreshPasteButton(data);
|
|
this.clipboardBroadcastChannel.postMessage(pollData);
|
|
deferred.resolve(pollData);
|
|
} else if (newStatus === "loading") {
|
|
setTimeout(checkStatus, 1_000);
|
|
} else {
|
|
deferred.reject();
|
|
throw new Error(`Unexpected clipboard status "${newStatus}" in successful API response.`);
|
|
}
|
|
})
|
|
}
|
|
setTimeout(checkStatus, 1_000);
|
|
return deferred;
|
|
} else {
|
|
this.postMessageForHideProcessingNotification();
|
|
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
duplicateXBlock: function(event) {
|
|
event.preventDefault();
|
|
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
|
|
const blockType = primaryHeader.attr('data-block-type');
|
|
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'duplicateXBlock',
|
|
message: 'Duplicate the XBlock',
|
|
payload: { blockType, usageId }
|
|
}, document.referrer
|
|
);
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Adjust the height of the dropdown menu',
|
|
payload: { courseXBlockDropdownHeight: 0 }
|
|
}, document.referrer
|
|
);
|
|
// Saves the height of the XBlock during duplication with the new editor.
|
|
// After closing the editor, the page scrolls to the newly created copy of the XBlock.
|
|
if (['html', 'problem', 'video'].includes(blockType)) {
|
|
const scrollHeight = event.clientY + this.findXBlockElement(event.target).height();
|
|
localStorage.setItem('modalEditLastYPosition', scrollHeight.toString());
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
this.duplicateComponent(this.findXBlockElement(event.target));
|
|
},
|
|
|
|
showMoveXBlockModal: function(event) {
|
|
var xblockElement = this.findXBlockElement(event.target),
|
|
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
|
|
sourceXBlockInfo = XBlockUtils.findXBlockInfo(xblockElement, this.model),
|
|
sourceParentXBlockInfo = XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
|
|
modal = new MoveXBlockModal({
|
|
sourceXBlockInfo: sourceXBlockInfo,
|
|
sourceParentXBlockInfo: sourceParentXBlockInfo,
|
|
XBlockURLRoot: this.getURLRoot(),
|
|
outlineURL: this.options.outlineURL
|
|
});
|
|
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'showMoveXBlockModal',
|
|
payload: {
|
|
sourceXBlockInfo: {
|
|
id: sourceXBlockInfo.attributes.id,
|
|
displayName: sourceXBlockInfo.attributes.display_name,
|
|
},
|
|
sourceParentXBlockInfo: {
|
|
id: sourceParentXBlockInfo.attributes.id,
|
|
category: sourceParentXBlockInfo.attributes.category,
|
|
hasChildren: sourceParentXBlockInfo.attributes.has_children,
|
|
},
|
|
},
|
|
}, document.referrer
|
|
);
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Adjust the height of the dropdown menu',
|
|
payload: { courseXBlockDropdownHeight: 0 }
|
|
}, document.referrer
|
|
);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
event.preventDefault();
|
|
modal.show();
|
|
},
|
|
|
|
deleteXBlock: function(event) {
|
|
event.preventDefault();
|
|
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
|
|
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
|
|
try {
|
|
if (this.options.isIframeEmbed) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'deleteXBlock',
|
|
message: 'Delete the XBlock',
|
|
payload: { usageId }
|
|
}, document.referrer
|
|
);
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'toggleCourseXBlockDropdown',
|
|
message: 'Adjust the height of the dropdown menu',
|
|
payload: { courseXBlockDropdownHeight: 0 }
|
|
}, document.referrer
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
this.deleteComponent(this.findXBlockElement(event.target));
|
|
},
|
|
|
|
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(),
|
|
$placeholderEl = $(this.createPlaceholderElement()),
|
|
requestData = _.extend(template, {
|
|
parent_locator: parentLocator
|
|
}),
|
|
scrollOffset,
|
|
placeholderElement,
|
|
$container;
|
|
|
|
if (this.options.isIframeEmbed && !this.isSplitTestContentPage) {
|
|
$container = $('ol.reorderable-container.ui-sortable');
|
|
scrollOffset = 0;
|
|
} else {
|
|
$container = listPanel;
|
|
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 && 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) {
|
|
// A placeholder element is created in the correct location for the duplicate 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 self = this,
|
|
parentElement = self.findXBlockElement(xblockElement.parent()),
|
|
scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
|
$placeholderEl = $(self.createPlaceholderElement()),
|
|
placeholderElement;
|
|
|
|
placeholderElement = $placeholderEl.insertAfter(xblockElement);
|
|
|
|
if (this.options.isIframeEmbed) {
|
|
this.postMessageToParent({
|
|
type: 'scrollToXBlock',
|
|
message: 'Scroll to XBlock',
|
|
payload: { scrollOffset: xblockElement.height() }
|
|
});
|
|
|
|
const messageHandler = ({ data }) => {
|
|
if (data && data.type === 'completeXBlockDuplicating') {
|
|
self.onNewXBlock(placeholderElement, null, true, data.payload);
|
|
window.removeEventListener('message', messageHandler);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageHandler);
|
|
|
|
return;
|
|
}
|
|
|
|
XBlockUtils.duplicateXBlock(xblockElement, parentElement)
|
|
.done(function(data) {
|
|
self.onNewXBlock(placeholderElement, scrollOffset, true, data);
|
|
})
|
|
.fail(function() {
|
|
// Remove the placeholder if the update failed
|
|
placeholderElement.remove();
|
|
});
|
|
},
|
|
|
|
deleteComponent: function(xblockElement) {
|
|
var self = this,
|
|
xblockInfo = new XBlockInfo({
|
|
id: xblockElement.data('locator')
|
|
});
|
|
|
|
if (this.options.isIframeEmbed) {
|
|
const messageHandler = ({ data }) => {
|
|
if (data && data.type === 'completeXBlockDeleting') {
|
|
const targetXBlockElement = $(`[data-locator="${data.payload.locator}"]`);
|
|
window.removeEventListener('message', messageHandler);
|
|
return self.onDelete(targetXBlockElement);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageHandler);
|
|
|
|
return;
|
|
}
|
|
|
|
XBlockUtils.deleteXBlock(xblockInfo).done(function() {
|
|
self.onDelete(xblockElement);
|
|
});
|
|
},
|
|
|
|
getSelectedLibraryComponents: function() {
|
|
var self = this;
|
|
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
|
|
$.getJSON(
|
|
ModuleUtils.getUpdateUrl(locator) + '/handler/get_block_ids',
|
|
function(data) {
|
|
self.selectedLibraryComponents = Array.from(data.source_block_ids);
|
|
self.storedSelectedLibraryComponents = Array.from(data.source_block_ids);
|
|
}
|
|
);
|
|
},
|
|
|
|
saveSelectedLibraryComponents: function(e) {
|
|
var self = this;
|
|
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
|
|
e.preventDefault();
|
|
$.postJSON(
|
|
ModuleUtils.getUpdateUrl(locator) + '/handler/submit_studio_edits',
|
|
{values: {source_block_ids: self.storedSelectedLibraryComponents}},
|
|
function() {
|
|
self.selectedLibraryComponents = Array.from(self.storedSelectedLibraryComponents);
|
|
self.toggleSaveButton();
|
|
}
|
|
);
|
|
},
|
|
|
|
toggleLibraryComponent: function(event) {
|
|
var componentId = $(event.target).closest('.studio-xblock-wrapper').data('locator');
|
|
var storeIndex = this.storedSelectedLibraryComponents.indexOf(componentId);
|
|
if (storeIndex > -1) {
|
|
this.storedSelectedLibraryComponents.splice(storeIndex, 1);
|
|
this.toggleSaveButton();
|
|
} else {
|
|
this.storedSelectedLibraryComponents.push(componentId);
|
|
this.toggleSaveButton();
|
|
}
|
|
},
|
|
|
|
viewXBlockContent: function(event) {
|
|
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;
|
|
}
|
|
},
|
|
|
|
toggleSaveButton: function() {
|
|
var $saveButton = $('.nav-actions .save-button');
|
|
if (JSON.stringify(this.selectedLibraryComponents.sort()) === JSON.stringify(this.storedSelectedLibraryComponents.sort())) {
|
|
$saveButton.addClass('is-hidden');
|
|
window.removeEventListener('beforeunload', this.onBeforePageUnloadCallback);
|
|
} else {
|
|
$saveButton.removeClass('is-hidden');
|
|
window.addEventListener('beforeunload', this.onBeforePageUnloadCallback);
|
|
}
|
|
},
|
|
|
|
onBeforePageUnloadCallback: function (event) {
|
|
event.preventDefault();
|
|
event.returnValue = '';
|
|
},
|
|
|
|
onDelete: function(xblockElement) {
|
|
// get the parent so we can remove this component from its parent.
|
|
var xblockView = this.xblockView,
|
|
parent = this.findXBlockElement(xblockElement.parent());
|
|
xblockElement.remove();
|
|
|
|
// Inform the runtime that the child has been deleted in case
|
|
// other views are listening to deletion events.
|
|
xblockView.acknowledgeXBlockDeletion(parent.data('locator'));
|
|
|
|
// Update publish and last modified information from the server.
|
|
this.model.fetch();
|
|
},
|
|
|
|
/*
|
|
* After move operation is complete, updates the xblock information from server .
|
|
*/
|
|
onXBlockMoved: function() {
|
|
this.model.fetch();
|
|
},
|
|
|
|
onNewXBlock: function(xblockElement, scrollOffset, is_duplicate, data) {
|
|
var useNewTextEditor = this.$('.xblock-header-primary').attr('use-new-editor-text'),
|
|
useNewVideoEditor = this.$('.xblock-header-primary').attr('use-new-editor-video'),
|
|
useVideoGalleryFlow = this.$('.xblock-header-primary').attr("use-video-gallery-flow"),
|
|
useNewProblemEditor = this.$('.xblock-header-primary').attr('use-new-editor-problem');
|
|
|
|
// find the block type in the locator if availible
|
|
if(data.hasOwnProperty('locator')) {
|
|
var matchBlockTypeFromLocator = /\@(.*?)\+/;
|
|
var blockType = data.locator.match(matchBlockTypeFromLocator);
|
|
}
|
|
// open mfe editors for new blocks only and not for content imported from libraries
|
|
if(!data.hasOwnProperty('upstreamRef') && ((useNewTextEditor === 'True' && blockType.includes('html'))
|
|
|| (useNewVideoEditor === 'True' && blockType.includes('video'))
|
|
|| (useNewProblemEditor === 'True' && blockType.includes('problem')))
|
|
){
|
|
if (this.options.isIframeEmbed && (this.isSplitTestContentPage || this.isVerticalContentPage)) {
|
|
return this.postMessageToParent({
|
|
type: 'handleRedirectToXBlockEditPage',
|
|
message: 'Redirect to xBlock edit page',
|
|
payload: {
|
|
type: blockType[1],
|
|
locator: encodeURI(data.locator),
|
|
},
|
|
});
|
|
}
|
|
var destinationUrl;
|
|
if (useVideoGalleryFlow === 'True' && blockType.includes('video')) {
|
|
destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/course-videos/' + encodeURI(data.locator);
|
|
}
|
|
else {
|
|
destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/' + blockType[1] + '/' + encodeURI(data.locator);
|
|
}
|
|
window.location.href = destinationUrl;
|
|
return;
|
|
}
|
|
if (!this.options.isIframeEmbed) {
|
|
ViewUtils.setScrollOffset(xblockElement, scrollOffset);
|
|
}
|
|
xblockElement.data('locator', data.locator);
|
|
return this.refreshXBlock(xblockElement, true, is_duplicate);
|
|
},
|
|
|
|
/**
|
|
* Refreshes the specified xblock's display. If the xblock is an inline child of a
|
|
* reorderable container then the element will be refreshed inline. If not, then the
|
|
* parent container will be refreshed instead.
|
|
* @param element An element representing the xblock to be refreshed.
|
|
* @param block_added Flag to indicate that new block has been just added.
|
|
*/
|
|
refreshXBlock: function(element, block_added, is_duplicate) {
|
|
var xblockElement = this.findXBlockElement(element),
|
|
parentElement = xblockElement.parent(),
|
|
rootLocator = this.xblockView.model.id;
|
|
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
|
|
if (block_added) {
|
|
this.render({refresh: true, block_added: block_added});
|
|
}
|
|
} else if (parentElement.hasClass('reorderable-container')) {
|
|
this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
|
|
} else {
|
|
this.refreshXBlock(this.findXBlockElement(parentElement));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh an xblock element inline on the page, using the specified xblockInfo.
|
|
* Note that the element is removed and replaced with the newly rendered xblock.
|
|
* @param xblockElement The xblock element to be refreshed.
|
|
* @param block_added Specifies if a block has been added, rather than just needs
|
|
* refreshing.
|
|
* @returns {jQuery promise} A promise representing the complete operation.
|
|
*/
|
|
refreshChildXBlock: function(xblockElement, block_added, is_duplicate) {
|
|
var self = this,
|
|
xblockInfo,
|
|
TemporaryXBlockView,
|
|
temporaryView;
|
|
xblockInfo = new XBlockInfo({
|
|
id: xblockElement.data('locator')
|
|
});
|
|
// There is only one Backbone view created on the container page, which is
|
|
// for the container xblock itself. Any child xblocks rendered inside the
|
|
// container do not get a Backbone view. Thus, create a temporary view
|
|
// to render the content, and then replace the original element with the result.
|
|
TemporaryXBlockView = XBlockView.extend({
|
|
updateHtml: function(element, html) {
|
|
// Replace the element with the new HTML content, rather than adding
|
|
// it as child elements.
|
|
this.$el = $(html).replaceAll(element); // xss-lint: disable=javascript-jquery-insertion
|
|
}
|
|
});
|
|
temporaryView = new TemporaryXBlockView({
|
|
model: xblockInfo,
|
|
view: self.xblockView.new_child_view,
|
|
el: xblockElement
|
|
});
|
|
return temporaryView.render({
|
|
success: function() {
|
|
self.onXBlockRefresh(temporaryView, block_added, is_duplicate);
|
|
temporaryView.unbind(); // Remove the temporary view
|
|
},
|
|
initRuntimeData: this
|
|
});
|
|
},
|
|
|
|
scrollToNewComponentButtons: function(event) {
|
|
event.preventDefault();
|
|
$.scrollTo(this.$('.add-xblock-component'), {duration: 250});
|
|
}
|
|
});
|
|
|
|
return XBlockContainerPage;
|
|
}); // end define();
|