feat: Tag List on Unit page (feature-flagged) (#33645)
This commit is contained in:
@@ -173,6 +173,10 @@ define(
|
||||
* True if summary configuration is enabled.
|
||||
*/
|
||||
summary_configuration_enabled: null,
|
||||
/**
|
||||
* List of tags of the unit. This list is managed by the content_tagging module.
|
||||
*/
|
||||
tags: null,
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/utils/string-utils',
|
||||
'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils',
|
||||
'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop',
|
||||
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',],
|
||||
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
|
||||
'js/views/utils/tagging_drawer_utils',],
|
||||
function(
|
||||
$, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils,
|
||||
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView
|
||||
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView,
|
||||
TaggingDrawerUtils
|
||||
) {
|
||||
var CourseOutlineView = XBlockOutlineView.extend({
|
||||
// takes XBlockOutlineInfo as a model
|
||||
@@ -458,41 +460,12 @@ function(
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
closeManageTagsDrawer(drawer, drawerCover) {
|
||||
$(drawerCover).css('display', 'none');
|
||||
$(drawer).empty();
|
||||
$(drawer).css('display', 'none');
|
||||
$('body').removeClass('drawer-open');
|
||||
},
|
||||
|
||||
openManageTagsDrawer(event) {
|
||||
const drawer = document.querySelector("#manage-tags-drawer");
|
||||
const drawerCover = document.querySelector(".drawer-cover")
|
||||
openManageTagsDrawer() {
|
||||
const article = document.querySelector('[data-taxonomy-tags-widget-url]');
|
||||
const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url');
|
||||
const contentId = this.model.get('id');
|
||||
|
||||
// Add handler to close drawer when dark background is clicked
|
||||
$(drawerCover).click(function() {
|
||||
this.closeManageTagsDrawer(drawer, drawerCover);
|
||||
}.bind(this));
|
||||
|
||||
// Add event listen to close drawer when close button is clicked from within the Iframe
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.data === 'closeManageTagsDrawer') {
|
||||
this.closeManageTagsDrawer(drawer, drawerCover)
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
$(drawerCover).css('display', 'block');
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
$(drawer).html(
|
||||
`<iframe src="${taxonomyTagsWidgetUrl}${contentId}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"></iframe>`
|
||||
);
|
||||
$(drawer).css('display', 'block');
|
||||
|
||||
// Prevent background from being scrollable when drawer is open
|
||||
$('body').addClass('drawer-open');
|
||||
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
|
||||
},
|
||||
|
||||
addButtonActions: function(element) {
|
||||
|
||||
@@ -8,10 +8,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
|
||||
'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',
|
||||
],
|
||||
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
|
||||
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
|
||||
ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView) {
|
||||
ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView, TaggingDrawerUtils) {
|
||||
'use strict';
|
||||
|
||||
var XBlockContainerPage = BasePage.extend({
|
||||
@@ -27,6 +28,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
'click .show-actions-menu-button': 'showXBlockActionsMenu',
|
||||
'click .new-component-button': 'scrollToNewComponentButtons',
|
||||
'click .paste-component-button': 'pasteComponent',
|
||||
'click .tags-button': 'openManageTags',
|
||||
},
|
||||
|
||||
options: {
|
||||
@@ -97,6 +99,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
});
|
||||
this.viewLiveActions.render();
|
||||
|
||||
this.tagListView = new ContainerSubviews.TagList({
|
||||
el: this.$('.unit-tags'),
|
||||
model: this.model
|
||||
});
|
||||
this.tagListView.render();
|
||||
|
||||
this.unitOutlineView = new UnitOutlineView({
|
||||
el: this.$('.wrapper-unit-overview'),
|
||||
model: this.model
|
||||
@@ -125,6 +133,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
xblockView = this.xblockView,
|
||||
loadingElement = this.$('.ui-loading'),
|
||||
unitLocationTree = this.$('.unit-location'),
|
||||
unitTags = this.$('.unit-tags'),
|
||||
hiddenCss = 'is-hidden';
|
||||
|
||||
loadingElement.removeClass(hiddenCss);
|
||||
@@ -150,6 +159,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
// 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();
|
||||
@@ -409,6 +419,13 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
this.duplicateComponent(this.findXBlockElement(event.target));
|
||||
},
|
||||
|
||||
openManageTags: function(event) {
|
||||
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
|
||||
const contentId = this.findXBlockElement(event.target).data('locator');
|
||||
|
||||
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
|
||||
},
|
||||
|
||||
showMoveXBlockModal: function(event) {
|
||||
var xblockElement = this.findXBlockElement(event.target),
|
||||
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* Subviews (usually small side panels) for XBlockContainerPage.
|
||||
*/
|
||||
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
|
||||
'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils'],
|
||||
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils) {
|
||||
'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'js/views/utils/tagging_drawer_utils'],
|
||||
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils, TaggingDrawerUtils) {
|
||||
'use strict';
|
||||
|
||||
var disabledCss = 'is-disabled';
|
||||
@@ -295,11 +296,175 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* TagList displays the tags of a unit.
|
||||
*/
|
||||
var TagList = BaseView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
events: {
|
||||
'click .wrapper-tag-header': 'expandTagContainer',
|
||||
'click .tagging-label': 'expandContentTag',
|
||||
'click .manage-tag-button': 'openManageTagDrawer',
|
||||
'keydown .wrapper-tag-header': 'handleKeyDownOnHeader',
|
||||
'keydown .tagging-label': 'handleKeyDownOnContentTag',
|
||||
'keydown .manage-tag-button': 'handleKeyDownOnTagDrawer',
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.template = this.loadTemplate('tag-list');
|
||||
this.model.on('sync', this.onSync, this);
|
||||
},
|
||||
|
||||
onSync: function(model) {
|
||||
if (ViewUtils.hasChangedAttributes(model, ['tags'])) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyDownOnHeader: function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.expandTagContainer();
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyDownOnContentTag: function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.expandContentTag(event);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyDownOnTagDrawer: function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.openManageTagDrawer();
|
||||
}
|
||||
},
|
||||
|
||||
expandTagContainer: function() {
|
||||
var $content = this.$('.wrapper-tags .wrapper-tag-content'),
|
||||
$header = this.$('.wrapper-tags .wrapper-tag-header'),
|
||||
$icon = this.$('.wrapper-tags .wrapper-tag-header .icon');
|
||||
|
||||
if ($content.hasClass('is-hidden')) {
|
||||
$content.removeClass('is-hidden');
|
||||
$icon.addClass('fa-caret-up');
|
||||
$icon.removeClass('fa-caret-down');
|
||||
$header.attr('aria-expanded', 'true');
|
||||
} else {
|
||||
$content.addClass('is-hidden');
|
||||
$icon.removeClass('fa-caret-up');
|
||||
$icon.addClass('fa-caret-down');
|
||||
$header.attr('aria-expanded', 'false');
|
||||
}
|
||||
},
|
||||
|
||||
expandContentTag: function(event) {
|
||||
var contentId = event.target.id,
|
||||
$content = this.$(`.wrapper-tags .content-tags-${contentId}`),
|
||||
$header = this.$(`.wrapper-tags .tagging-label-${contentId}`),
|
||||
$icon = this.$(`.wrapper-tags .tagging-label-${contentId} .icon`);
|
||||
|
||||
if ($content.hasClass('is-hidden')) {
|
||||
$content.removeClass('is-hidden');
|
||||
$icon.addClass('fa-caret-up');
|
||||
$icon.removeClass('fa-caret-down');
|
||||
$header.attr('aria-expanded', 'true');
|
||||
} else {
|
||||
$content.addClass('is-hidden');
|
||||
$icon.removeClass('fa-caret-up');
|
||||
$icon.addClass('fa-caret-down');
|
||||
$header.attr('aria-expanded', 'false');
|
||||
}
|
||||
},
|
||||
|
||||
renderTagElements: function(tags, depth, parentId) {
|
||||
const tagListElement = this;
|
||||
tags.forEach(function(tag) {
|
||||
const parentElement = document.querySelector(`.content-tags-${parentId}`);
|
||||
var tagContentElement = document.createElement('div'),
|
||||
tagValueElement = document.createElement('span');
|
||||
|
||||
// Element that contains the tag value and the arrow icon
|
||||
tagContentElement.style.marginLeft = `${depth}em`;
|
||||
tagContentElement.className = `tagging-label tagging-label-tag-${tag.id}`;
|
||||
tagContentElement.id = `tag-${tag.id}`;
|
||||
|
||||
// Element that contains the tag value
|
||||
tagValueElement.textContent = tag.value;
|
||||
tagValueElement.id = `tag-${tag.id}`;
|
||||
tagValueElement.className = 'tagging-label-value';
|
||||
|
||||
tagContentElement.appendChild(tagValueElement);
|
||||
parentElement.appendChild(tagContentElement);
|
||||
|
||||
if (tag.children.length > 0) {
|
||||
var tagIconElement = document.createElement('span'),
|
||||
tagChildrenElement = document.createElement('div');
|
||||
|
||||
// Arrow icon
|
||||
tagIconElement.className = 'icon fa fa-caret-down';
|
||||
tagIconElement.ariaHidden = 'true';
|
||||
tagIconElement.id = `tag-${tag.id}`;
|
||||
|
||||
// Element that contains the children of this tag
|
||||
tagChildrenElement.className = `content-tags-tag-${tag.id} is-hidden`;
|
||||
|
||||
tagContentElement.tabIndex = 0;
|
||||
tagContentElement.role = "button";
|
||||
tagContentElement.ariaExpanded = "false";
|
||||
tagContentElement.setAttribute('aria-controls', `content-tags-tag-${tag.id}`);
|
||||
tagContentElement.appendChild(tagIconElement);
|
||||
parentElement.appendChild(tagChildrenElement);
|
||||
|
||||
// Render children
|
||||
tagListElement.renderTagElements(tag.children, depth + 1, `tag-${tag.id}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderTags: function() {
|
||||
if (this.model.get('tags') !== null) {
|
||||
const taxonomies = this.model.get('tags').taxonomies;
|
||||
const tagListElement = this;
|
||||
taxonomies.forEach(function(taxonomy) {
|
||||
tagListElement.renderTagElements(taxonomy.tags, 1, `tax-${taxonomy.id}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
openManageTagDrawer: function() {
|
||||
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
|
||||
const contentId = this.model.get('id');
|
||||
|
||||
TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
HtmlUtils.HTML(
|
||||
this.template({
|
||||
tags: this.model.get('tags'),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.renderTags();
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
MessageView: MessageView,
|
||||
ViewLiveButtonController: ViewLiveButtonController,
|
||||
Publisher: Publisher,
|
||||
PublishHistory: PublishHistory,
|
||||
ContainerAccess: ContainerAccess
|
||||
ContainerAccess: ContainerAccess,
|
||||
TagList: TagList
|
||||
};
|
||||
}); // end define();
|
||||
|
||||
55
cms/static/js/views/utils/tagging_drawer_utils.js
Normal file
55
cms/static/js/views/utils/tagging_drawer_utils.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Provides utilities to open and close the tagging drawer to manage tags.
|
||||
*
|
||||
* To use this drawer you need to add the following code into your template:
|
||||
*
|
||||
* ```
|
||||
* <div id="manage-tags-drawer" class="drawer"></div>
|
||||
* <div class="drawer-cover"></div>
|
||||
* ```
|
||||
*/
|
||||
define(['jquery'],
|
||||
function($,) {
|
||||
'use strict';
|
||||
|
||||
var closeDrawer, openDrawer;
|
||||
|
||||
closeDrawer = function(drawer, drawerCover) {
|
||||
$(drawerCover).css('display', 'none');
|
||||
$(drawer).empty();
|
||||
$(drawer).css('display', 'none');
|
||||
$('body').removeClass('drawer-open');
|
||||
};
|
||||
|
||||
openDrawer = function(taxonomyTagsWidgetUrl, contentId) {
|
||||
const drawer = document.querySelector("#manage-tags-drawer");
|
||||
const drawerCover = document.querySelector(".drawer-cover");
|
||||
|
||||
// Add handler to close drawer when dark background is clicked
|
||||
$(drawerCover).click(function() {
|
||||
closeDrawer(drawer, drawerCover);
|
||||
}.bind(this));
|
||||
|
||||
// Add event listen to close drawer when close button is clicked from within the Iframe
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.data === 'closeManageTagsDrawer') {
|
||||
closeDrawer(drawer, drawerCover)
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
$(drawerCover).css('display', 'block');
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
$(drawer).html(
|
||||
`<iframe src="${taxonomyTagsWidgetUrl}${contentId}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"></iframe>`
|
||||
);
|
||||
$(drawer).css('display', 'block');
|
||||
|
||||
// Prevent background from being scrollable when drawer is open
|
||||
$('body').addClass('drawer-open');
|
||||
};
|
||||
|
||||
return {
|
||||
openDrawer: openDrawer,
|
||||
closeDrawer: closeDrawer
|
||||
};
|
||||
});
|
||||
@@ -119,6 +119,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
// inverse primary button
|
||||
%btn-primary-inverse {
|
||||
@extend %ui-btn-primary;
|
||||
|
||||
background: theme-color("inverse");
|
||||
border-color: theme-color("primary");
|
||||
color: theme-color("primary");
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: theme-color("primary");
|
||||
border-color: $uxpl-blue-hover-active;
|
||||
color: theme-color("inverse");
|
||||
}
|
||||
|
||||
&.current,
|
||||
&.active {
|
||||
background: theme-color("primary");
|
||||
border-color: $uxpl-blue-hover-active;
|
||||
color: theme-color("inverse");
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: theme-color("primary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +Secondary Button - Extends
|
||||
// ====================
|
||||
// gray secondary button
|
||||
|
||||
@@ -242,6 +242,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
.unit-tags {
|
||||
.wrapper-tags {
|
||||
margin-bottom: $baseline;
|
||||
padding: ($baseline*0.75);
|
||||
background-color: $white;
|
||||
|
||||
.wrapper-tag-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.tag-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background-color: $gray-l5;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-tag-header:focus {
|
||||
border: 1px dotted gray;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@extend %btn-primary-inverse;
|
||||
|
||||
width: 100%;
|
||||
margin: 16px 2px 8px 2px;
|
||||
}
|
||||
|
||||
.wrapper-tag-content {
|
||||
background-color: $white;
|
||||
|
||||
.content-taxonomies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10px;
|
||||
|
||||
.tagging-label {
|
||||
display: flex;
|
||||
padding: 4px 0px;
|
||||
|
||||
.tagging-label-value {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tagging-label-count {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.tagging-label:hover,
|
||||
.tagging-label:focus {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// versioning widget
|
||||
.unit-publish-history {
|
||||
.wrapper-last-publish {
|
||||
|
||||
Reference in New Issue
Block a user