1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * Description of import/upgrade into Moodle:
18 * 1.) Download from https://github.com/pathable/truncate
19 * 2.) Copy jquery.truncate.js into lib/amd/src/truncate.js
20 * 3.) Edit truncate.js to return the $.truncate function as truncate
21 * 4.) Apply Moodle changes from git commit 7172b33e241c4d42cff01f78bf8570408f43fdc2
25 * Module for text truncation.
27 * Implementation provided by Pathable (thanks!).
28 * See: https://github.com/pathable/truncate
30 * @module core/truncate
33 * @copyright 2017 Pathable
35 * 2017 Ryan Wyllie <ryan@moodle.com>
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 define(['jquery'], function($) {
40 // Matches trailing non-space characters.
41 var chop = /(\s*\S+|\s)$/;
43 // Matches the first word in the string.
46 // Matches any space characters.
49 // Special thanks to Mathias Bynens for the multi-byte char
50 // implementation. Much love.
51 // see: https://github.com/mathiasbynens/String.prototype.at/blob/master/at.js
52 var charLengthAt = function(text, position) {
53 var string = String(text);
54 var size = string.length;
56 var index = position ? Number(position) : 0;
57 if (index != index) { // better `isNaN`
60 // Account for out-of-bounds indices
61 // The odd lower bound is because the ToInteger operation is
62 // going to round `n` to `0` for `-1 < n <= 0`.
63 if (index <= -1 || index >= size) {
66 // Second half of `ToInteger`
68 // Get the first code unit and code unit value
69 var cuFirst = string.charCodeAt(index);
71 var nextIndex = index + 1;
73 if ( // Check if it’s the start of a surrogate pair.
74 cuFirst >= 0xD800 && cuFirst <= 0xDBFF && // high surrogate
75 size > nextIndex // there is a next code unit
77 cuSecond = string.charCodeAt(nextIndex);
78 if (cuSecond >= 0xDC00 && cuSecond <= 0xDFFF) { // low surrogate
85 var lengthMultiByte = function(text) {
88 for (var i = 0; i < text.length; i += charLengthAt(text, i)) {
95 var getSliceLength = function(text, amount) {
104 length += charLengthAt(text, length);
106 } while (length < text.length && count < amount);
111 // Return a truncated html string. Delegates to $.fn.truncate.
112 $.truncate = function(html, options) {
113 return $('<div></div>').append(html).truncate(options).html();
116 // Truncate the contents of an element in place.
117 $.fn.truncate = function(options) {
118 if (!isNaN(parseFloat(options))) options = {length: options};
119 var o = $.extend({}, $.truncate.defaults, options);
121 return this.each(function() {
124 if (o.noBreaks) self.find('br').replaceWith(' ');
126 var ellipsisLength = o.ellipsis.length;
127 var text = self.text();
128 var textLength = lengthMultiByte(text);
129 var excess = textLength - o.length + ellipsisLength;
131 if (textLength < o.length) return;
132 if (o.stripTags) self.text(text);
134 // Chop off any partial words if appropriate.
135 if (o.words && excess > 0) {
136 var sliced = text.slice(0, getSliceLength(text, o.length - ellipsisLength) + 1);
137 var replaced = sliced.replace(chop, '');
138 var truncated = lengthMultiByte(replaced);
139 var oneWord = sliced.match(space) ? false : true;
141 if (o.keepFirstWord && truncated === 0) {
142 excess = textLength - lengthMultiByte(start.exec(text)[0]) - ellipsisLength;
143 } else if (oneWord && truncated === 0) {
144 excess = textLength - o.length + ellipsisLength;
146 excess = textLength - truncated - 1;
150 // The requested length is larger than the text. No need for ellipsis.
151 if (excess > textLength) {
152 excess = textLength - o.length;
155 if (excess < 0 || !excess && !o.truncated) return;
157 // Iterate over each child node in reverse, removing excess text.
158 $.each(self.contents().get().reverse(), function(i, el) {
160 var text = $el.text();
161 var length = lengthMultiByte(text);
163 // If the text is longer than the excess, remove the node and continue.
164 if (length <= excess) {
171 // Remove the excess text and append the ellipsis.
172 if (el.nodeType === 3) {
173 var splitAmount = length - excess;
174 splitAmount = splitAmount >= 0 ? getSliceLength(text, splitAmount) : 0;
175 $(el.splitText(splitAmount)).replaceWith(o.ellipsis);
179 // Recursively truncate child nodes.
180 $el.truncate($.extend(o, {length: length - excess + ellipsisLength}));
186 $.truncate.defaults = {
188 // Strip all html elements, leaving only plain text.
191 // Only truncate at word boundaries.
194 // When 'words' is active, keeps the first word in the string
195 // even if it's longer than a target length.
196 keepFirstWord: false,
198 // Replace instances of <br> with a single space.
201 // The maximum length of the truncated html.
204 // The character to use as the ellipsis. The word joiner (U+2060) can be
205 // used to prevent a hanging ellipsis, but displays incorrectly in Chrome
207 // http://code.google.com/p/chromium/issues/detail?id=68323
208 //ellipsis: '\u2026' // '\u2060\u2026'
209 ellipsis: '\u2026' // '\u2060\u2026'
214 truncate: $.truncate,