Merge pull request #23632 from edx/mikix/welcome-message

Cut off long course welcome messages
This commit is contained in:
Michael Terry
2020-04-07 12:06:37 -04:00
committed by GitHub
9 changed files with 187 additions and 4 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');
});
});
});

View File

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

View File

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