forked from openedx/edx-platform
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [AXIMST-28] expose editor for advanced xblocks (#2528)
* feat: expose editor for advanced xblocks * feat: added logic for displaying and hiding the xblock modal editing window * refactor: container_editor template refactoring --------- Co-authored-by: PKulkoRaccoonGang <[email protected]> Co-authored-by: Іван Нєдєльніцев <[email protected]>
- Loading branch information
1 parent
ece19b7
commit 9fd6606
Showing
3 changed files
with
340 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
## coding=utf-8 | ||
## mako | ||
|
||
## Pages currently use v1 styling by default. Once the Pattern Library | ||
## rollout has been completed, this default can be switched to v2. | ||
<%! main_css = "style-main-v1" %> | ||
|
||
## Standard imports | ||
<%namespace name='static' file='static_content.html'/> | ||
<%! | ||
from django.utils.translation import gettext as _ | ||
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES | ||
from lms.djangoapps.branding import api as branding_api | ||
from openedx.core.djangoapps.util.user_messages import PageLevelMessages | ||
from openedx.core.djangolib.js_utils import ( | ||
dump_js_escaped_json, js_escaped_string | ||
) | ||
from openedx.core.djangolib.markup import HTML | ||
from openedx.core.release import RELEASE_LINE | ||
%> | ||
<%def name="online_help_token()"> | ||
<% | ||
return "container" | ||
%> | ||
</%def> | ||
<%! | ||
from django.urls import reverse | ||
from django.utils.translation import gettext as _ | ||
from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name | ||
from openedx.core.djangolib.js_utils import ( | ||
dump_js_escaped_json, js_escaped_string | ||
) | ||
from openedx.core.djangolib.markup import HTML, Text | ||
%> | ||
|
||
<%page expression_filter="h"/> | ||
<!doctype html> | ||
<!--[if lte IE 9]><html class="ie9 lte9" lang="${LANGUAGE_CODE}"><![endif]--> | ||
<!--[if !IE]><<!--><html lang="${LANGUAGE_CODE}"><!--<![endif]--> | ||
<head dir="${static.dir_rtl()}"> | ||
<meta charset="utf-8"> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||
<meta name="openedx-release-line" content="${RELEASE_LINE}" /> | ||
<title> | ||
<%block name="title"> | ||
${xblock.display_name_with_default} ${xblock_type_display_name(xblock)} | ||
</%block> | | ||
% if context_course: | ||
<% ctx_loc = context_course.location %> | ||
${context_course.display_name_with_default} | | ||
% elif context_library: | ||
${context_library.display_name_with_default} | | ||
% endif | ||
${settings.STUDIO_NAME} | ||
</title> | ||
|
||
<% | ||
jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE) | ||
%> | ||
|
||
% if getattr(settings, 'CAPTURE_CONSOLE_LOG', False): | ||
<script type="text/javascript"> | ||
const oldOnError = window.onerror; | ||
window.localStorage.setItem('console_log_capture', JSON.stringify([])); | ||
|
||
window.onerror = function (message, url, lineno, colno, error) { | ||
if (oldOnError) { | ||
oldOnError.apply(this, arguments); | ||
} | ||
|
||
const messages = JSON.parse(window.localStorage.getItem('console_log_capture')); | ||
messages.push([message, url, lineno, colno, (error || {}).stack]); | ||
window.localStorage.setItem('console_log_capture', JSON.stringify(messages)); | ||
} | ||
</script> | ||
% endif | ||
|
||
<script type="text/javascript" src="${static.url(jsi18n_path)}"></script> | ||
% if settings.DEBUG: | ||
## Provides a fallback for gettext functions in development environment | ||
<script type="text/javascript" src="${static.url('js/src/gettext_fallback.js')}"></script> | ||
% endif | ||
<meta name="viewport" content="width=device-width,initial-scale=1"> | ||
<meta name="path_prefix" content="${EDX_ROOT_URL}"> | ||
<%block name="header_meta"></%block> | ||
<% favicon_url = branding_api.get_favicon_url() %> | ||
<link rel="icon" type="image/x-icon" href="${favicon_url}"/> | ||
<%static:css group='style-vendor'/> | ||
<%static:css group='style-vendor-tinymce-content'/> | ||
<%static:css group='style-vendor-tinymce-skin'/> | ||
<style> | ||
html body { | ||
background: transparent; | ||
} | ||
</style> | ||
|
||
% if uses_bootstrap: | ||
<link rel="stylesheet" href="${static.url(self.attr.main_css)}" type="text/css" media="all" /> | ||
% else: | ||
<%static:css group='${self.attr.main_css}'/> | ||
% endif | ||
|
||
<%include file="widgets/segment-io.html" /> | ||
<%block name="header_extras"> | ||
|
||
% for template_name in templates: | ||
<script type="text/template" id="${template_name}-tpl"> | ||
<%static:include path="js/${template_name}.underscore" /> | ||
</script> | ||
% endfor | ||
<script type="text/template" id="image-modal-tpl"> | ||
<%static:include path="common/templates/image-modal.underscore" /> | ||
</script> | ||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> | ||
% if not settings.STUDIO_FRONTEND_CONTAINER_URL: | ||
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/common.min.css')}" /> | ||
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/editImageModal.min.css')}" /> | ||
% endif | ||
|
||
</%block> | ||
<!-- Hotjar Tracking Code for studio --> | ||
<script> | ||
(function(h, o, t, j, a, r){ | ||
h.hj = h.hj || function() { (h.hj.q = h.hj.q || []).push(arguments) }; | ||
h._hjSettings={ hjid: Number('${settings.HOTJAR_ID |n, js_escaped_string}'), hjsv: 6 }; | ||
a = o.getElementsByTagName('head')[0]; | ||
r = o.createElement('script'); | ||
r.async = 1; | ||
r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv; | ||
a.appendChild(r); | ||
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); | ||
</script> | ||
</head> | ||
|
||
<body class="${static.dir_rtl()} <%block name='bodyclass'></%block> lang_${LANGUAGE_CODE} view-container"> | ||
<%block name="view_notes"></%block> | ||
<a class="nav-skip" href="#main">${_("Skip to main content")}</a> | ||
<%static:js group='base_vendor'/> | ||
<%static:webpack entry="commons"/> | ||
<script type="text/javascript"> | ||
window.baseUrl = '${settings.STATIC_URL | n, js_escaped_string}'; | ||
require.config({ baseUrl: window.baseUrl }); | ||
</script> | ||
<script type="text/javascript" src="${static.url("cms/js/require-config.js")}"></script> | ||
<!-- view --> | ||
<div class="wrapper wrapper-view" dir="${static.dir_rtl()}"> | ||
<% | ||
banner_messages = list(PageLevelMessages.user_messages(request)) | ||
%> | ||
<main id="main" aria-label="Content" tabindex="-1"> | ||
<div id="content"> | ||
<%block name="content"> | ||
<script type="text/javascript"> | ||
window.STUDIO_FRONTEND_IN_CONTEXT_IMAGE_SELECTION = true; | ||
</script> | ||
|
||
<div style="display:none" class="wrapper-mast wrapper"> | ||
<header class="mast has-actions has-navigation has-subtitle"> | ||
<nav class="nav-actions" aria-label="${_('Page Actions')}"> | ||
<ul> | ||
<li class="action-item action-edit nav-item"> | ||
<a href="#" class="button button-edit action-button edit-button"> | ||
<span class="icon fa fa-pencil" aria-hidden="true"></span> | ||
<span class="action-button-text">${_("Edit")}</span> | ||
</a> | ||
</li> | ||
</ul> | ||
</nav> | ||
</header> | ||
</div> | ||
|
||
<script type="text/javascript"> | ||
$(document).ready(() => { | ||
// Serves to initialize the rendering of a xblock edit modal window. | ||
setTimeout(() => $('.button-edit').trigger('click'), 300); | ||
|
||
/** | ||
* Callback function for the MutationObserver to handle mutations | ||
* and send information when the modal window close logic is triggered. | ||
* | ||
* @callback mutationCallback | ||
* @param {MutationRecord[]} mutations - The list of mutations detected by the observer. | ||
*/ | ||
const xblockEditModalObserver = new MutationObserver((mutations) => { | ||
const modalClassName = 'wrapper-modal-window-edit-xblock'; | ||
|
||
// When a modal window is opened while the template is rendering, | ||
// an element with class modalClassName is rendered, | ||
// the MutationObserver defines this process in removedNodes. | ||
const modalElementMutationRecords = mutations | ||
.filter(({ removedNodes }) => { | ||
const filteredModalClassName = Array.from(removedNodes).filter((node) => | ||
node.className && node.className.includes(modalClassName)); | ||
|
||
return filteredModalClassName.length > 0; | ||
}); | ||
|
||
// If the element was present and deleted, close the modal window. | ||
if (modalElementMutationRecords.length > 0 && !$('.' + modalClassName).length) { | ||
window.parent.postMessage({ | ||
method: 'close_edit_modal', | ||
msg: 'Sends a message when the modal window is closed' | ||
}, '*'); | ||
} | ||
}); | ||
|
||
xblockEditModalObserver.observe(document, { | ||
childList: true, | ||
subtree: true, | ||
attributes: true, | ||
attributeFilter: ['class'] | ||
}); | ||
}); | ||
</script> | ||
</%block> | ||
</div> | ||
</main> | ||
</div> | ||
|
||
<%block name="modal_placeholder"></%block> | ||
<%block name="jsextra"></%block> | ||
|
||
% if context_course: | ||
<%static:webpack entry="js/factories/context_course"/> | ||
<script type="text/javascript"> | ||
window.course = new ContextCourse({ | ||
id: '${context_course.id | n, js_escaped_string}', | ||
name: '${context_course.display_name_with_default | n, js_escaped_string}', | ||
url_name: '${context_course.location.block_id | n, js_escaped_string}', | ||
org: '${context_course.location.org | n, js_escaped_string}', | ||
num: '${context_course.location.course | n, js_escaped_string}', | ||
display_course_number: '${context_course.display_coursenumber | n, js_escaped_string}', | ||
revision: '${context_course.location.branch | n, js_escaped_string}', | ||
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json }, | ||
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json}, | ||
start: ${context_course.start | n, dump_js_escaped_json}, | ||
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json} | ||
}); | ||
</script> | ||
% endif | ||
|
||
% if user.is_authenticated: | ||
<%static:webpack entry='js/sock'/> | ||
% endif | ||
|
||
<%block name='page_bundle'> | ||
<script type="text/javascript"> | ||
require(['js/factories/base'], function () { | ||
<%block name='requirejs'></%block> | ||
}); | ||
</script> | ||
<%static:webpack entry="js/factories/container"> | ||
ContainerFactory( | ||
${component_templates | n, dump_js_escaped_json}, | ||
${xblock_info | n, dump_js_escaped_json}, | ||
'${action | n, js_escaped_string}', | ||
{ | ||
isUnitPage: ${is_unit_page | n, dump_js_escaped_json}, | ||
canEdit: true, | ||
outlineURL: '${outline_url | n, js_escaped_string}', | ||
clipboardData: ${user_clipboard | n, dump_js_escaped_json}, | ||
} | ||
); | ||
|
||
require(['js/models/xblock_info', 'js/views/xblock', 'js/views/utils/xblock_utils', 'common/js/components/utils/view_utils', 'gettext'], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) { | ||
var model = new XBlockInfo({ id: '${subsection.location|n, decode.utf8}' }); | ||
var xblockView = new XBlockView({ | ||
model: model, | ||
el: $('#sequence-nav'), | ||
view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}', | ||
clipboardData: ${user_clipboard | n, dump_js_escaped_json}, | ||
}); | ||
|
||
xblockView.xblockReady = function() { | ||
var toggleCaretButton = function(clipboardData) { | ||
if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes('vertical')) { | ||
$('.dropdown-toggle-button').show(); | ||
} else { | ||
$('.dropdown-toggle-button').hide(); | ||
$('.dropdown-options').hide(); | ||
} | ||
}; | ||
this.clipboardBroadcastChannel = new BroadcastChannel('studio_clipboard_channel'); | ||
this.clipboardBroadcastChannel.onmessage = (event) => toggleCaretButton(event.data); | ||
toggleCaretButton(this.options.clipboardData); | ||
|
||
$('#new-unit-button').on('click', function(event) { | ||
event.preventDefault(); | ||
XBlockUtils.addXBlock($(this)).done(function(locator) { | ||
ViewUtils.redirect('/container/' + locator + '?action=new'); | ||
}); | ||
}); | ||
|
||
$('.custom-dropdown .dropdown-toggle-button').on('click', function(event) { | ||
event.stopPropagation(); // Prevent the event from closing immediately when we open it | ||
$(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility | ||
var isExpanded = $(this).attr('aria-expanded') === 'true'; | ||
$(this).attr('aria-expanded', !isExpanded); | ||
}); | ||
|
||
$('.seq_paste_unit').on('click', function(event) { | ||
event.preventDefault(); | ||
$('.dropdown-options').hide(); | ||
XBlockUtils.pasteXBlock($(this)).done(function(data) { | ||
ViewUtils.redirect('/container/' + data.locator + '?action=new'); | ||
}); | ||
}); | ||
}; | ||
|
||
xblockView.render(); | ||
}); | ||
</%static:webpack> | ||
</%block> | ||
|
||
<div class="modal-cover"></div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters