1256 lines
58 KiB
JavaScript
1256 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.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 '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')))
|
|
){
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
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();
|