From d74d91f49a6e4009e883efe6693299601365ad33 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 20 Feb 2017 07:45:43 +0000 Subject: [PATCH 1/4] MDL-57972 javascript: Add truncate.js third party lib Part of MDL-55611 --- .eslintignore | 1 + .stylelintignore | 1 + lib/amd/build/truncate.min.js | 1 + lib/amd/src/truncate.js | 125 ++++++++++++++++++++++++++++++++++ lib/thirdpartylibs.xml | 6 ++ 5 files changed, 134 insertions(+) create mode 100644 lib/amd/build/truncate.min.js create mode 100644 lib/amd/src/truncate.js diff --git a/.eslintignore b/.eslintignore index 452b0df9d05..f44ca3e0f0a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -56,6 +56,7 @@ lib/amd/src/chartjs-lazy.js lib/maxmind/GeoIp2/ lib/maxmind/MaxMind/ lib/ltiprovider/ +lib/amd/src/truncate.js media/player/videojs/amd/src/video-lazy.js media/player/videojs/amd/src/Youtube-lazy.js media/player/videojs/videojs/ diff --git a/.stylelintignore b/.stylelintignore index 48f1959ef89..d0d279a7fc9 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -57,6 +57,7 @@ lib/amd/src/chartjs-lazy.js lib/maxmind/GeoIp2/ lib/maxmind/MaxMind/ lib/ltiprovider/ +lib/amd/src/truncate.js media/player/videojs/amd/src/video-lazy.js media/player/videojs/amd/src/Youtube-lazy.js media/player/videojs/videojs/ diff --git a/lib/amd/build/truncate.min.js b/lib/amd/build/truncate.min.js new file mode 100644 index 00000000000..a1d02a00cc8 --- /dev/null +++ b/lib/amd/build/truncate.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=/(\s*\S+|\s)$/,c=/^(\S*)/;return a.truncate=function(b,c){return a("
").append(b).truncate(c).html()},a.fn.truncate=function(d){a.isNumeric(d)&&(d={length:d});var e=a.extend({},a.truncate.defaults,d);return this.each(function(){var d=a(this);e.noBreaks&&d.find("br").replaceWith(" ");var f=d.text(),g=f.length-e.length;if(e.stripTags&&d.text(f),e.words&&g>0){var h=f.slice(0,e.length).replace(b,"").length;g=e.keepFirstWord&&0===h?f.length-c.exec(f)[0].length-1:f.length-h-1}g<0||!g&&!e.truncated||a.each(d.contents().get().reverse(),function(b,c){var d=a(c),f=d.text(),h=f.length;return h<=g?(e.truncated=!0,g-=h,void d.remove()):3===c.nodeType?(a(c.splitText(h-g-1)).replaceWith(e.ellipsis),!1):(d.truncate(a.extend(e,{length:h-g})),!1)})})},a.truncate.defaults={stripTags:!1,words:!1,keepFirstWord:!1,noBreaks:!1,length:1/0,ellipsis:"…"},{truncate:a.truncate}}); \ No newline at end of file diff --git a/lib/amd/src/truncate.js b/lib/amd/src/truncate.js new file mode 100644 index 00000000000..d7e7cd53d55 --- /dev/null +++ b/lib/amd/src/truncate.js @@ -0,0 +1,125 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Module for text truncation. + * + * Implementation provided by Pathable (thanks!). + * See: https://github.com/pathable/truncate + * + * @module core/truncate + * @package core + * @class truncate + * @copyright 2017 Pathable + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], function($) { + + // Matches trailing non-space characters. + var chop = /(\s*\S+|\s)$/; + + // Matches the first word in the string. + var start = /^(\S*)/; + + // Return a truncated html string. Delegates to $.fn.truncate. + $.truncate = function(html, options) { + return $('
').append(html).truncate(options).html(); + }; + + // Truncate the contents of an element in place. + $.fn.truncate = function(options) { + if ($.isNumeric(options)) options = {length: options}; + var o = $.extend({}, $.truncate.defaults, options); + + return this.each(function() { + var self = $(this); + + if (o.noBreaks) self.find('br').replaceWith(' '); + + var text = self.text(); + var excess = text.length - o.length; + + if (o.stripTags) self.text(text); + + // Chop off any partial words if appropriate. + if (o.words && excess > 0) { + var truncated = text.slice(0, o.length).replace(chop, '').length; + + if (o.keepFirstWord && truncated === 0) { + excess = text.length - start.exec(text)[0].length - 1; + } else { + excess = text.length - truncated - 1; + } + } + + if (excess < 0 || !excess && !o.truncated) return; + + // Iterate over each child node in reverse, removing excess text. + $.each(self.contents().get().reverse(), function(i, el) { + var $el = $(el); + var text = $el.text(); + var length = text.length; + + // If the text is longer than the excess, remove the node and continue. + if (length <= excess) { + o.truncated = true; + excess -= length; + $el.remove(); + return; + } + + // Remove the excess text and append the ellipsis. + if (el.nodeType === 3) { + $(el.splitText(length - excess - 1)).replaceWith(o.ellipsis); + return false; + } + + // Recursively truncate child nodes. + $el.truncate($.extend(o, {length: length - excess})); + return false; + }); + }); + }; + + $.truncate.defaults = { + + // Strip all html elements, leaving only plain text. + stripTags: false, + + // Only truncate at word boundaries. + words: false, + + // When 'words' is active, keeps the first word in the string + // even if it's longer than a target length. + keepFirstWord: false, + + // Replace instances of
with a single space. + noBreaks: false, + + // The maximum length of the truncated html. + length: Infinity, + + // The character to use as the ellipsis. The word joiner (U+2060) can be + // used to prevent a hanging ellipsis, but displays incorrectly in Chrome + // on Windows 7. + // http://code.google.com/p/chromium/issues/detail?id=68323 + ellipsis: '\u2026' // '\u2060\u2026' + + }; + + return { + truncate: $.truncate, + }; +}); diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index c13421f3769..6468dd21d43 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -291,4 +291,10 @@ 3.0.2 2.0 + + amd/src/truncate.js + Truncate.js + MIT + 0.0.1 + From 7172b33e241c4d42cff01f78bf8570408f43fdc2 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 20 Feb 2017 07:47:40 +0000 Subject: [PATCH 2/4] MDL-57972 javascript: Change truncate.js behaviour Updated truncate.js to behave closer to the moodle implementation of shorten_text. Part of MDL-55611 --- lib/amd/build/truncate.min.js | 2 +- lib/amd/src/truncate.js | 100 +++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/lib/amd/build/truncate.min.js b/lib/amd/build/truncate.min.js index a1d02a00cc8..156328eb6ab 100644 --- a/lib/amd/build/truncate.min.js +++ b/lib/amd/build/truncate.min.js @@ -1 +1 @@ -define(["jquery"],function(a){var b=/(\s*\S+|\s)$/,c=/^(\S*)/;return a.truncate=function(b,c){return a("
").append(b).truncate(c).html()},a.fn.truncate=function(d){a.isNumeric(d)&&(d={length:d});var e=a.extend({},a.truncate.defaults,d);return this.each(function(){var d=a(this);e.noBreaks&&d.find("br").replaceWith(" ");var f=d.text(),g=f.length-e.length;if(e.stripTags&&d.text(f),e.words&&g>0){var h=f.slice(0,e.length).replace(b,"").length;g=e.keepFirstWord&&0===h?f.length-c.exec(f)[0].length-1:f.length-h-1}g<0||!g&&!e.truncated||a.each(d.contents().get().reverse(),function(b,c){var d=a(c),f=d.text(),h=f.length;return h<=g?(e.truncated=!0,g-=h,void d.remove()):3===c.nodeType?(a(c.splitText(h-g-1)).replaceWith(e.ellipsis),!1):(d.truncate(a.extend(e,{length:h-g})),!1)})})},a.truncate.defaults={stripTags:!1,words:!1,keepFirstWord:!1,noBreaks:!1,length:1/0,ellipsis:"…"},{truncate:a.truncate}}); \ No newline at end of file +define(["jquery"],function(a){var b=/(\s*\S+|\s)$/,c=/^(\S*)/,d=/\s/,e=function(a,b){if(null==this)throw TypeError();var c=String(a),d=c.length,e=b?Number(b):0;if(e!=e&&(e=0),e<=-1||e>=d)return"";e=0|e;var f,g=c.charCodeAt(e),h=e+1,i=1;return g>=55296&&g<=56319&&d>h&&(f=c.charCodeAt(h),f>=56320&&f<=57343&&(i=2)),i},f=function(a){for(var b=0,c=0;c").append(b).truncate(c).html()},a.fn.truncate=function(e){a.isNumeric(e)&&(e={length:e});var h=a.extend({},a.truncate.defaults,e);return this.each(function(){var e=a(this);h.noBreaks&&e.find("br").replaceWith(" ");var i=h.ellipsis.length,j=e.text(),k=f(j),l=k-h.length+i;if(!(k0){var m=j.slice(0,g(j,h.length-i)+1),n=m.replace(b,""),o=f(n),p=!m.match(d);l=h.keepFirstWord&&0===o?k-f(c.exec(j)[0])-i:p&&0===o?k-h.length+i:k-o-1}l>k&&(l=k-h.length),l<0||!l&&!h.truncated||a.each(e.contents().get().reverse(),function(b,c){var d=a(c),e=d.text(),j=f(e);if(j<=l)return h.truncated=!0,l-=j,void d.remove();if(3===c.nodeType){var k=j-l;return k=k>=0?g(e,k):0,a(c.splitText(k)).replaceWith(h.ellipsis),!1}return d.truncate(a.extend(h,{length:j-l+i})),!1})}})},a.truncate.defaults={stripTags:!1,words:!1,keepFirstWord:!1,noBreaks:!1,length:1/0,ellipsis:"…"},{truncate:a.truncate}}); \ No newline at end of file diff --git a/lib/amd/src/truncate.js b/lib/amd/src/truncate.js index d7e7cd53d55..e9ba1f24556 100644 --- a/lib/amd/src/truncate.js +++ b/lib/amd/src/truncate.js @@ -23,6 +23,8 @@ * @package core * @class truncate * @copyright 2017 Pathable + * 2017 Mathias Bynens + * 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery'], function($) { @@ -33,6 +35,74 @@ define(['jquery'], function($) { // Matches the first word in the string. var start = /^(\S*)/; + // Matches any space characters. + var space = /\s/; + + // Special thanks to Mathias Bynens for the multi-byte char + // implementation. Much love. + // see: https://github.com/mathiasbynens/String.prototype.at/blob/master/at.js + var charLengthAt = function(text, position) { + if (this == null) { + throw TypeError(); + } + var string = String(text); + var size = string.length; + // `ToInteger` + var index = position ? Number(position) : 0; + if (index != index) { // better `isNaN` + index = 0; + } + // Account for out-of-bounds indices + // The odd lower bound is because the ToInteger operation is + // going to round `n` to `0` for `-1 < n <= 0`. + if (index <= -1 || index >= size) { + return ''; + } + // Second half of `ToInteger` + index = index | 0; + // Get the first code unit and code unit value + var cuFirst = string.charCodeAt(index); + var cuSecond; + var nextIndex = index + 1; + var len = 1; + if ( // Check if it’s the start of a surrogate pair. + cuFirst >= 0xD800 && cuFirst <= 0xDBFF && // high surrogate + size > nextIndex // there is a next code unit + ) { + cuSecond = string.charCodeAt(nextIndex); + if (cuSecond >= 0xDC00 && cuSecond <= 0xDFFF) { // low surrogate + len = 2; + } + } + return len; + }; + + var lengthMultiByte = function(text) { + var count = 0; + + for (var i = 0; i < text.length; i += charLengthAt(text, i)) { + count++; + } + + return count; + }; + + var getSliceLength = function(text, amount) { + if (!text.length) { + return 0; + } + + var length = 0; + var count = 0; + + do { + length += charLengthAt(text, length); + count++; + } while (length < text.length && count < amount); + + return length; + }; + // Return a truncated html string. Delegates to $.fn.truncate. $.truncate = function(html, options) { return $('
').append(html).truncate(options).html(); @@ -48,29 +118,42 @@ define(['jquery'], function($) { if (o.noBreaks) self.find('br').replaceWith(' '); + var ellipsisLength = o.ellipsis.length; var text = self.text(); - var excess = text.length - o.length; + var textLength = lengthMultiByte(text); + var excess = textLength - o.length + ellipsisLength; + if (textLength < o.length) return; if (o.stripTags) self.text(text); // Chop off any partial words if appropriate. if (o.words && excess > 0) { - var truncated = text.slice(0, o.length).replace(chop, '').length; + var sliced = text.slice(0, getSliceLength(text, o.length - ellipsisLength) + 1); + var replaced = sliced.replace(chop, ''); + var truncated = lengthMultiByte(replaced); + var oneWord = sliced.match(space) ? false : true; if (o.keepFirstWord && truncated === 0) { - excess = text.length - start.exec(text)[0].length - 1; + excess = textLength - lengthMultiByte(start.exec(text)[0]) - ellipsisLength; + } else if (oneWord && truncated === 0) { + excess = textLength - o.length + ellipsisLength; } else { - excess = text.length - truncated - 1; + excess = textLength - truncated - 1; } } + // The requested length is larger than the text. No need for ellipsis. + if (excess > textLength) { + excess = textLength - o.length; + } + if (excess < 0 || !excess && !o.truncated) return; // Iterate over each child node in reverse, removing excess text. $.each(self.contents().get().reverse(), function(i, el) { var $el = $(el); var text = $el.text(); - var length = text.length; + var length = lengthMultiByte(text); // If the text is longer than the excess, remove the node and continue. if (length <= excess) { @@ -82,12 +165,14 @@ define(['jquery'], function($) { // Remove the excess text and append the ellipsis. if (el.nodeType === 3) { - $(el.splitText(length - excess - 1)).replaceWith(o.ellipsis); + var splitAmount = length - excess; + splitAmount = splitAmount >= 0 ? getSliceLength(text, splitAmount) : 0; + $(el.splitText(splitAmount)).replaceWith(o.ellipsis); return false; } // Recursively truncate child nodes. - $el.truncate($.extend(o, {length: length - excess})); + $el.truncate($.extend(o, {length: length - excess + ellipsisLength})); return false; }); }); @@ -115,6 +200,7 @@ define(['jquery'], function($) { // used to prevent a hanging ellipsis, but displays incorrectly in Chrome // on Windows 7. // http://code.google.com/p/chromium/issues/detail?id=68323 + //ellipsis: '\u2026' // '\u2060\u2026' ellipsis: '\u2026' // '\u2060\u2026' }; From 180f5f23a68df1aeba34786d43a5ea764d087ac3 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Tue, 7 Mar 2017 05:21:47 +0000 Subject: [PATCH 3/4] MDL-57972 javascript: add upgrade instructions for truncate.js Part of MDL-55611 --- lib/amd/src/truncate.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/amd/src/truncate.js b/lib/amd/src/truncate.js index e9ba1f24556..0c708dfbb1a 100644 --- a/lib/amd/src/truncate.js +++ b/lib/amd/src/truncate.js @@ -13,6 +13,14 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * Description of import/upgrade into Moodle: + * 1.) Download from https://github.com/pathable/truncate + * 2.) Copy jquery.truncate.js into lib/amd/src/truncate.js + * 3.) Edit truncate.js to return the $.truncate function as truncate + * 4.) Apply Moodle changes from git commit 7172b33e241c4d42cff01f78bf8570408f43fdc2 + */ + /** * Module for text truncation. * From 75378ded5ff36f034bb3ba2434fa43f824b4351b Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Fri, 17 Feb 2017 01:15:46 +0000 Subject: [PATCH 4/4] MDL-57972 mustache: add shortentext template helper Part of MDL-55611 --- lib/amd/build/templates.min.js | 2 +- lib/amd/src/templates.js | 36 ++++++++++- .../output/mustache_shorten_text_helper.php | 64 +++++++++++++++++++ lib/outputrenderers.php | 4 +- 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 lib/classes/output/mustache_shorten_text_helper.php diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js index e73b2f7fa51..b3f4325e597 100644 --- a/lib/amd/build/templates.min.js +++ b/lib/amd/build/templates.min.js @@ -1 +1 @@ -define(["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/log","core/config","core/localstorage","core/event","core/yui","core/log"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=0,n={},o={},p=function(){this.requiredStrings=[],this.requiredJS=[],this.currentThemeName=""};p.prototype.requiredStrings=null,p.prototype.requiredJS=null,p.prototype.currentThemeName="",p.prototype.getTemplate=function(a){var d=a.split("/"),e=d.shift(),f=d.shift(),g=this.currentThemeName+"/"+a;if(g in o)return o[g];var h=i.get("core_template/"+g);if(h)return n[g]=h,o[g]=b.Deferred().resolve(h).promise(),o[g];var j=c.call([{methodname:"core_output_load_template",args:{component:e,template:f,themename:this.currentThemeName}}],!0,!1);return o[g]=j[0].then(function(a){return n[g]=a,i.set("core_template/"+g,a),a}),o[g]},p.prototype.partialHelper=function(a){var b=this.currentThemeName+"/"+a;return b in n||e.exception(new Error("Failed to pre-fetch the template: "+a)),n[b]},p.prototype.pixHelper=function(b,c,d){var e,g=c.split(","),h="",i="",j="";g.length>0&&(h=g.shift().trim()),g.length>0&&(i=g.shift().trim()),g.length>0&&(j=g.join(",").trim());var k=f.imageUrl(h,i),l={attributes:[{name:"src",value:k},{name:"alt",value:d(j)},{name:"title",value:d(j)},{name:"class",value:"smallicon"}]},m=this.currentThemeName+"/core/pix_icon",o=n[m];return e=a.render(o,l,this.partialHelper.bind(this)),e.trim()},p.prototype.jsHelper=function(a,b,c){return this.requiredJS.push(c(b,a)),""},p.prototype.stringHelper=function(a,b,c){var d=b.split(","),e="",f="",g="";d.length>0&&(e=d.shift().trim()),d.length>0&&(f=d.shift().trim()),d.length>0&&(g=d.join(",").trim()),""!==g&&(g=c(g,a)),0===g.indexOf("{")&&0!==g.indexOf("{{")&&(g=JSON.parse(g));var h=this.requiredStrings.length;return this.requiredStrings.push({key:e,component:f,param:g}),"[[_s"+h+"]]"},p.prototype.quoteHelper=function(a,b,c){var d=c(b.trim(),a);return d=d.replace('"','\\"').replace(/([\{\}]{2,3})/g,"{{=<% %>=}}$1<%={{ }}=%>"),'"'+d+'"'},p.prototype.addHelpers=function(a,b){this.currentThemeName=b,this.requiredStrings=[],this.requiredJS=[],a.uniqid=m++,a.str=function(){return this.stringHelper.bind(this,a)}.bind(this),a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this),a.js=function(){return this.jsHelper.bind(this,a)}.bind(this),a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this),a.globals={config:h},a.currentTheme=b},p.prototype.getJS=function(a){var b="";return this.requiredJS.length>0&&(b=this.requiredJS.join(";\n")),this.treatStringsInContent(b,a)},p.prototype.treatStringsInContent=function(a,b){var c,d,e,f,g,h,i=/\[\[_s\d+\]\]/;do{for(c="",d=a.search(i);d>-1;){c+=a.substring(0,d),a=a.substr(d),e="",f=4,g=a.substr(f,1);do e+=g,f++,g=a.substr(f,1);while("]"!=g);h=b[parseInt(e,10)],"undefined"==typeof h&&(l.debug("Could not find string for pattern [[_s"+e+"]]."),h=""),c+=h,a=a.substr(6+e.length),d=a.search(i)}a=c+a,d=a.search(i)}while(d>-1);return a},p.prototype.doRender=function(c,e,f){return this.currentThemeName=f,this.getTemplate("core/pix_icon").then(function(){this.addHelpers(e,f);var g=a.render(c,e,this.partialHelper.bind(this));return this.requiredStrings.length>0?d.get_strings(this.requiredStrings).then(function(a){return g=this.treatStringsInContent(g,a),b.Deferred().resolve(g,this.getJS(a)).promise()}.bind(this)):b.Deferred().resolve(g.trim(),this.getJS([])).promise()}.bind(this))};var q=function(a){if(""!==a.trim()){var c=b("