Merge pull request #23632 from edx/mikix/welcome-message
Cut off long course welcome messages
This commit is contained in:
53
common/static/common/js/utils/clamp-html.js
Normal file
53
common/static/common/js/utils/clamp-html.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Used to ellipsize a section of arbitrary HTML after a specified number of words.
|
||||
*
|
||||
* Note: this will modify the DOM structure of root in place.
|
||||
* To keep the original around, you may want to save the result of cloneNode(true) before calling this method.
|
||||
*
|
||||
* Known bug: This method will ignore any special whitespace in the source and simply output single spaces.
|
||||
* Which means that will not be respected. This is not considered worth solving at time of writing.
|
||||
*
|
||||
* Returns how many words remain (or a negative number if the content got clamped)
|
||||
*/
|
||||
function clampHtmlByWords(root, wordsLeft) {
|
||||
'use strict';
|
||||
|
||||
var remaining = wordsLeft;
|
||||
var nodes = Array.from(root.childNodes ? root.childNodes : []);
|
||||
var words, chopped;
|
||||
|
||||
// First, cut short any text in our node, as necessary
|
||||
if (root.nodeName === '#text' && root.data) {
|
||||
// split on words, ignoring any resulting empty strings
|
||||
words = root.data.split(/\s+/).filter(Boolean);
|
||||
if (remaining < 0) {
|
||||
root.data = ''; // eslint-disable-line no-param-reassign
|
||||
} else if (remaining > words.length) {
|
||||
remaining -= words.length;
|
||||
} else {
|
||||
// OK, let's add an ellipsis and cut some of root.data
|
||||
chopped = words.slice(0, remaining).join(' ') + '…';
|
||||
// But be careful to get any preceding space too
|
||||
if (root.data.match(/^\s/)) {
|
||||
chopped = ' ' + chopped;
|
||||
}
|
||||
root.data = chopped; // eslint-disable-line no-param-reassign
|
||||
remaining = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now do the same for any child nodes
|
||||
nodes.forEach(function(node) {
|
||||
if (remaining < 0) {
|
||||
root.removeChild(node);
|
||||
} else {
|
||||
remaining = clampHtmlByWords(node, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
return remaining;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clampHtmlByWords: clampHtmlByWords
|
||||
};
|
||||
33
common/static/common/js/utils/clamp-html.test.jsx
Normal file
33
common/static/common/js/utils/clamp-html.test.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { clampHtmlByWords } from './clamp-html';
|
||||
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
});
|
||||
|
||||
describe('ClampHtml', () => {
|
||||
test.each([
|
||||
['', 0, ''],
|
||||
['a b', 0, '…'],
|
||||
['a b', 1, 'a…'],
|
||||
['a b c', 2, 'a b…'],
|
||||
['a <i>aa ab</i> b', 2, 'a <i>aa…</i>'],
|
||||
['a <i>aa ab</i> <em>ac</em>', 2, 'a <i>aa…</i>'],
|
||||
['a <i>aa <em>aaa</em></i>', 2, 'a <i>aa…</i>'],
|
||||
['a <i>aa <em>aaa</em> ab</i>', 3, 'a <i>aa <em>aaa…</em></i>'],
|
||||
['a <i>aa ab</i> b c', 4, 'a <i>aa ab</i> b…'],
|
||||
])('clamps by words: %s, %i', (input, wordsLeft, expected) => {
|
||||
const div = ReactDOM.render(<div dangerouslySetInnerHTML={{ __html: input }} />, container);
|
||||
clampHtmlByWords(div, wordsLeft);
|
||||
expect(div.innerHTML).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -201,6 +201,13 @@
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-message-show-more {
|
||||
float: right;
|
||||
background-color: transparent;
|
||||
color: $black;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Course sidebar
|
||||
@@ -406,7 +413,7 @@
|
||||
|
||||
#expand-collapse-outline-all-button {
|
||||
float: right;
|
||||
background-color: $white;
|
||||
background-color: transparent;
|
||||
color: $black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ RELATIVE_DATES_FLAG = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_date
|
||||
# Waffle flag to enable user calendar syncing
|
||||
CALENDAR_SYNC_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'calendar_sync')
|
||||
|
||||
# Waffle flag to ellipsize course welcome messages if they are too long
|
||||
SHORTEN_WELCOME_MESSAGE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'shorten_welcome_message')
|
||||
|
||||
|
||||
def course_home_page_title(course): # pylint: disable=unused-argument
|
||||
"""
|
||||
|
||||
@@ -5,5 +5,28 @@
|
||||
<span class="icon fa fa-times" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
This is a useful welcome message!
|
||||
|
||||
<div id="welcome-message-content" class="welcome-message-content">
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
This is a useful welcome message that is too long!
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
id="welcome-message-show-more"
|
||||
class="btn btn-primary welcome-message-show-more"
|
||||
aria-live="polite"
|
||||
data-state="more"
|
||||
hidden
|
||||
>
|
||||
Show More
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* globals $ */
|
||||
import 'jquery.cookie';
|
||||
import gettext from 'gettext'; // eslint-disable-line
|
||||
import { clampHtmlByWords } from 'common/js/utils/clamp-html'; // eslint-disable-line
|
||||
|
||||
export class WelcomeMessage { // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -35,5 +37,29 @@ export class WelcomeMessage { // eslint-disable-line import/prefer-default-expo
|
||||
}
|
||||
}
|
||||
$('.dismiss-message button').click(() => WelcomeMessage.dismissWelcomeMessage(options.dismissUrl));
|
||||
|
||||
|
||||
// "Show More" support for welcome messages
|
||||
const messageContent = document.querySelector('#welcome-message-content');
|
||||
const fullText = messageContent.innerHTML;
|
||||
if (options.shortenWelcomeMessage && clampHtmlByWords(messageContent, 100) < 0) {
|
||||
const showMoreButton = document.querySelector('#welcome-message-show-more');
|
||||
const shortText = messageContent.innerHTML;
|
||||
|
||||
showMoreButton.removeAttribute('hidden');
|
||||
|
||||
showMoreButton.addEventListener('click', (event) => {
|
||||
if (showMoreButton.getAttribute('data-state') === 'less') {
|
||||
showMoreButton.textContent = gettext('Show More');
|
||||
messageContent.innerHTML = shortText;
|
||||
showMoreButton.setAttribute('data-state', 'more');
|
||||
} else {
|
||||
showMoreButton.textContent = gettext('Show Less');
|
||||
messageContent.innerHTML = fullText;
|
||||
showMoreButton.setAttribute('data-state', 'less');
|
||||
}
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,27 @@ describe('Welcome Message factory', () => {
|
||||
requests.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortened welcome message', () => {
|
||||
const endpointUrl = '/course/course_id/dismiss_message/';
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('course_experience/fixtures/welcome-message-fragment.html');
|
||||
new WelcomeMessage({ // eslint-disable-line no-new
|
||||
dismissUrl: endpointUrl,
|
||||
shortenWelcomeMessage: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('Shortened message can be toggled', () => {
|
||||
expect($('#welcome-message-content').text()).toContain('…');
|
||||
expect($('#welcome-message-show-more').text()).toContain('Show More');
|
||||
$('#welcome-message-show-more').click();
|
||||
expect($('#welcome-message-content').text()).not.toContain('…');
|
||||
expect($('#welcome-message-show-more').text()).toContain('Show Less');
|
||||
$('#welcome-message-show-more').click();
|
||||
expect($('#welcome-message-content').text()).toContain('…');
|
||||
expect($('#welcome-message-show-more').text()).toContain('Show More');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
%>
|
||||
|
||||
@@ -18,12 +18,25 @@ from openedx.core.djangolib.markup import HTML
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${HTML(welcome_message_html)}
|
||||
<div id="welcome-message-content" class="welcome-message-content">
|
||||
${HTML(welcome_message_html)}
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
id="welcome-message-show-more"
|
||||
class="btn btn-primary welcome-message-show-more"
|
||||
aria-live="polite"
|
||||
data-state="more"
|
||||
hidden
|
||||
>
|
||||
${_("Show More")}
|
||||
</button>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%static:webpack entry="WelcomeMessage">
|
||||
new WelcomeMessage({
|
||||
dismissUrl: "${dismiss_url | n, js_escaped_string}",
|
||||
shortenWelcomeMessage: ${shorten_welcome_message | n, dump_js_escaped_json},
|
||||
});
|
||||
</%static:webpack>
|
||||
|
||||
@@ -14,6 +14,7 @@ from web_fragments.fragment import Fragment
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag
|
||||
from openedx.features.course_experience import SHORTEN_WELCOME_MESSAGE_FLAG
|
||||
|
||||
from .course_updates import get_ordered_updates
|
||||
|
||||
@@ -43,6 +44,7 @@ class WelcomeMessageFragmentView(EdxFragmentView):
|
||||
context = {
|
||||
'dismiss_url': dismiss_url,
|
||||
'welcome_message_html': welcome_message_html,
|
||||
'shorten_welcome_message': SHORTEN_WELCOME_MESSAGE_FLAG.is_enabled(course_key),
|
||||
}
|
||||
|
||||
if get_course_tag(request.user, course_key, PREFERENCE_KEY) == 'False':
|
||||
|
||||
Reference in New Issue
Block a user