feat: Tag List on Unit page (feature-flagged) (#33645)

This commit is contained in:
Chris Chávez
2023-11-17 13:03:28 -05:00
committed by GitHub
parent f87006f778
commit 104d42c67a
13 changed files with 501 additions and 44 deletions

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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'),

View File

@@ -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();

View 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
};
});

View File

@@ -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

View File

@@ -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 {