MDL-62801 themes: Remove old mustache caches when new one generated
[moodle.git] / lib / amd / src / truncate.js
blob0c708dfbb1ab70e0466fb3f0867ddd2962cd137b
1 // This file is part of Moodle - http://moodle.org/
2 //
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.
7 //
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/>.
16 /**
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
22  */
24 /**
25  * Module for text truncation.
26  *
27  * Implementation provided by Pathable (thanks!).
28  * See: https://github.com/pathable/truncate
29  *
30  * @module     core/truncate
31  * @package    core
32  * @class      truncate
33  * @copyright  2017 Pathable
34  *             2017 Mathias Bynens
35  *             2017 Ryan Wyllie <ryan@moodle.com>
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 define(['jquery'], function($) {
40   // Matches trailing non-space characters.
41   var chop = /(\s*\S+|\s)$/;
43   // Matches the first word in the string.
44   var start = /^(\S*)/;
46   // Matches any space characters.
47   var space = /\s/;
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     if (this == null) {
54       throw TypeError();
55     }
56     var string = String(text);
57     var size = string.length;
58     // `ToInteger`
59     var index = position ? Number(position) : 0;
60     if (index != index) { // better `isNaN`
61       index = 0;
62     }
63     // Account for out-of-bounds indices
64     // The odd lower bound is because the ToInteger operation is
65     // going to round `n` to `0` for `-1 < n <= 0`.
66     if (index <= -1 || index >= size) {
67       return '';
68     }
69     // Second half of `ToInteger`
70     index = index | 0;
71     // Get the first code unit and code unit value
72     var cuFirst = string.charCodeAt(index);
73     var cuSecond;
74     var nextIndex = index + 1;
75     var len = 1;
76     if ( // Check if it’s the start of a surrogate pair.
77       cuFirst >= 0xD800 && cuFirst <= 0xDBFF && // high surrogate
78       size > nextIndex // there is a next code unit
79     ) {
80       cuSecond = string.charCodeAt(nextIndex);
81       if (cuSecond >= 0xDC00 && cuSecond <= 0xDFFF) { // low surrogate
82         len = 2;
83       }
84     }
85     return len;
86   };
88   var lengthMultiByte = function(text) {
89     var count = 0;
91     for (var i = 0; i < text.length; i += charLengthAt(text, i)) {
92       count++;
93     }
95     return count;
96   };
98   var getSliceLength = function(text, amount) {
99     if (!text.length) {
100       return 0;
101     }
103     var length = 0;
104     var count = 0;
106     do {
107       length += charLengthAt(text, length);
108       count++;
109     } while (length < text.length && count < amount);
111     return length;
112   };
114   // Return a truncated html string.  Delegates to $.fn.truncate.
115   $.truncate = function(html, options) {
116     return $('<div></div>').append(html).truncate(options).html();
117   };
119   // Truncate the contents of an element in place.
120   $.fn.truncate = function(options) {
121     if ($.isNumeric(options)) options = {length: options};
122     var o = $.extend({}, $.truncate.defaults, options);
124     return this.each(function() {
125       var self = $(this);
127       if (o.noBreaks) self.find('br').replaceWith(' ');
129       var ellipsisLength = o.ellipsis.length;
130       var text = self.text();
131       var textLength = lengthMultiByte(text);
132       var excess = textLength - o.length + ellipsisLength;
134       if (textLength < o.length) return;
135       if (o.stripTags) self.text(text);
137       // Chop off any partial words if appropriate.
138       if (o.words && excess > 0) {
139         var sliced = text.slice(0, getSliceLength(text, o.length - ellipsisLength) + 1);
140         var replaced = sliced.replace(chop, '');
141         var truncated = lengthMultiByte(replaced);
142         var oneWord = sliced.match(space) ? false : true;
144         if (o.keepFirstWord && truncated === 0) {
145           excess = textLength - lengthMultiByte(start.exec(text)[0]) - ellipsisLength;
146         } else if (oneWord && truncated === 0) {
147           excess = textLength - o.length + ellipsisLength;
148         } else {
149           excess = textLength - truncated - 1;
150         }
151       }
153       // The requested length is larger than the text. No need for ellipsis.
154       if (excess > textLength) {
155         excess = textLength - o.length;
156       }
158       if (excess < 0 || !excess && !o.truncated) return;
160       // Iterate over each child node in reverse, removing excess text.
161       $.each(self.contents().get().reverse(), function(i, el) {
162         var $el = $(el);
163         var text = $el.text();
164         var length = lengthMultiByte(text);
166         // If the text is longer than the excess, remove the node and continue.
167         if (length <= excess) {
168           o.truncated = true;
169           excess -= length;
170           $el.remove();
171           return;
172         }
174         // Remove the excess text and append the ellipsis.
175         if (el.nodeType === 3) {
176           var splitAmount = length - excess;
177           splitAmount = splitAmount >= 0 ? getSliceLength(text, splitAmount) : 0;
178           $(el.splitText(splitAmount)).replaceWith(o.ellipsis);
179           return false;
180         }
182         // Recursively truncate child nodes.
183         $el.truncate($.extend(o, {length: length - excess + ellipsisLength}));
184         return false;
185       });
186     });
187   };
189   $.truncate.defaults = {
191     // Strip all html elements, leaving only plain text.
192     stripTags: false,
194     // Only truncate at word boundaries.
195     words: false,
197     // When 'words' is active, keeps the first word in the string
198     // even if it's longer than a target length.
199     keepFirstWord: false,
201     // Replace instances of <br> with a single space.
202     noBreaks: false,
204     // The maximum length of the truncated html.
205     length: Infinity,
207     // The character to use as the ellipsis.  The word joiner (U+2060) can be
208     // used to prevent a hanging ellipsis, but displays incorrectly in Chrome
209     // on Windows 7.
210     // http://code.google.com/p/chromium/issues/detail?id=68323
211     //ellipsis: '\u2026' // '\u2060\u2026'
212     ellipsis: '\u2026' // '\u2060\u2026'
214   };
216     return {
217         truncate: $.truncate,
218     };