-
Notifications
You must be signed in to change notification settings - Fork 17
/
jquery.shorten.js
268 lines (204 loc) · 8.56 KB
/
jquery.shorten.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/*
* Shorten, a jQuery plugin to automatically shorten text to fit in a block or a pre-set width and configure how the text ends.
* Copyright (C) 2009-2013 Marc Diethelm
* License: (GPL 3, http://www.gnu.org/licenses/gpl-3.0.txt) see license.txt
*/
(function ($) {
//var $c = console;
var
_native = false,
is_canvasTextSupported,
measureContextInit, // canvas context or table cell
measureText, // function that measures text width
info_identifier = "shorten-info",
options_identifier = "shorten-options";
$.fn.shorten = function() {
var userOptions = {},
args = arguments, // for better minification
func = args.callee, // dito; and shorter than $.fn.shorten
options;
if ( args.length ) {
if ( args[0].constructor == Object ) {
userOptions = args[0];
} else if ( args[0] == "options" ) {
return $(this).eq(0).data(options_identifier);
} else {
userOptions = {
width: parseInt(args[0], 10),
tail: args[1]
}
}
}
this.css("visibility", "hidden"); // Hide the element(s) while manipulating them
// apply options vs. defaults
options = $.extend({}, func.defaults, userOptions);
/**
* HERE WE GO!
**/
return this.each(function () {
var
$this = $(this),
text = $this.text(),
numChars = text.length,
measureContext = measureContextInit.call( this ),
origLength = measureText.call( this, text, measureContext ),
targetWidth,
tailText = $("<span/>").html(options.tail).text(), // convert html to text
info = {
shortened: false,
textOverflow: false
}
if ($this.css("float") != "none") {
targetWidth = options.width || $this.width(); // this let's correctly shorten text in floats, but fucks up the rest
} else {
targetWidth = options.width || $this.parent().width();
}
if (targetWidth < 0) { // jQuery versions < 1.4.4 return negative values for .width() if display:none is used.
//$c.log("nonsense target width ", targetWidth);
return true;
}
$this.data(options_identifier, options);
// for consistency with the text-overflow method (which requires these properties), but not actually neccessary.
this.style.display = "block";
//this.style.overflow = "hidden"; // firefox: a floated li will cause the ul to have a "bottom padding" if this is set.
this.style.whiteSpace = "nowrap";
if ( origLength < targetWidth ) {
//$c.log("nothing to do");
$this.text( text );
this.style.visibility = "visible";
// if this is a re-run on this element (and tooltip is enabled), remove obsolete tooltip
if ($this.data(info_identifier) && options.tooltip) {
$this.removeAttr("title");
}
$this.data(info_identifier, info);
return true;
}
if ( options.tooltip ) {
this.setAttribute("title", text);
}
/**
* If browser implements text-overflow:ellipsis in CSS and tail is …/Unicode 8230/(…), use it!
* In this case we're doing the measurement above to determine if we need the tooltip.
**/
if ( func._native && !userOptions.width ) {
//$c.log("css ellipsis");
var rendered_tail = $("<span>"+options.tail+"</span>").text(); // render tail to find out if it's the ellipsis character.
if ( rendered_tail.length == 1 && rendered_tail.charCodeAt(0) == 8230 ) {
$this.text( text );
// the following three properties are needed for text-overflow to work (tested in Chrome).
// for consistency now I need to set this everywhere... which probably interferes with users' layout...
//this.style.whiteSpace = "nowrap";
this.style.overflow = "hidden";
//this.style.display = "block";
this.style[func._native] = "ellipsis";
this.style.visibility = "visible";
info.shortened = true;
info.textOverflow = "ellipsis";
$this.data(info_identifier, info);
return true;
}
}
var tailWidth = measureText.call( this, tailText, measureContext ); // convert html to text and measure it
targetWidth = targetWidth - tailWidth;
//$c.log(text +" + "+ tailText);
/**
* Before we start removing characters one by one, let's try to be more intelligent about this:
* If the original string is longer than targetWidth by at least 15% (for safety), then shorten it
* to targetWidth + 15% (and re-measure for safety). If the resulting text still is too long (as expected),
* use that for further shortening. Else use the original text. This saves a lot of time for text that is
* much longer than the desired width.
*/
var safeGuess = targetWidth * 1.15; // add 15% to targetWidth for safety before making the cut.
if ( origLength - safeGuess > 0 ) { // if it's safe to cut, do it.
var cut_ratio = safeGuess / origLength,
num_guessText_chars = Math.ceil( numChars * cut_ratio ),
// looking good: shorten and measure
guessText = text.substring(0, num_guessText_chars),
guessTextLength = measureText.call( this, guessText, measureContext );
//$c.info("safe guess: remove " + (numChars - num_guessText_chars) +" chars");
if ( guessTextLength > targetWidth ) { // make sure it's not too short!
text = guessText;
numChars = text.length;
}
}
// Remove characters one by one until text width <= targetWidth
//var count = 0;
do {
numChars--;
text = text.substring(0, numChars);
//count++;
} while ( measureText.call( this, text, measureContext ) >= targetWidth );
$this.html( $.trim( $("<span/>").text(text).html() ) + options.tail );
this.style.visibility = "visible";
//$c.info(count + " normal truncating cycles...")
//$c.log("----------------------------------------------------------------------");
info.shortened = true;
$this.data(info_identifier, info);
return true;
});
return true;
};
var css = document.documentElement.style;
if ( "textOverflow" in css ) {
_native = "textOverflow";
} else if ( "OTextOverflow" in css ) {
_native = "OTextOverflow";
}
// test for canvas support
if ( typeof Modernizr != 'undefined' && Modernizr.canvastext ) { // if Modernizr has tested for this already use that.
is_canvasTextSupported = Modernizr.canvastext;
} else {
var canvas = document.createElement("canvas");
is_canvasTextSupported = !!(canvas.getContext && canvas.getContext("2d") && (typeof canvas.getContext("2d").fillText === 'function'));
}
$.fn.shorten._is_canvasTextSupported = is_canvasTextSupported;
$.fn.shorten._native = _native;
// decide on a method for measuring text width
if ( is_canvasTextSupported ) {
//$c.log("canvas");
measureContextInit = measureText_initCanvas;
measureText = measureText_canvas;
} else {
//$c.log("table")
measureContextInit = measureText_initTable;
measureText = measureText_table;
}
function measureText_initCanvas()
{
var $this = $(this);
var canvas = document.createElement("canvas");
//scanvas.setAttribute("width", 500); canvas.setAttribute("height", 40);
ctx = canvas.getContext("2d");
$this.html( canvas );
/* the rounding is experimental. it fixes a problem with a font size specified as 0.7em which resulted in a computed size of 11.2px.
without rounding the measured font was too small. even with rounding the result differs slightly from the table method's results. */
// Get the current text style. This string uses the same syntax as the CSS font specifier. The order matters!
ctx.font = $this.css("font-style") +" "+ $this.css("font-variant") +" "+ $this.css("font-weight") +" "+ Math.ceil(parseFloat($this.css("font-size"))) +"px "+ $this.css("font-family");
return ctx;
}
// measurement using canvas
function measureText_canvas( text, ctx )
{
//ctx.fillStyle = "red"; ctx.fillRect (0, 0, 500, 40);
//ctx.fillStyle = "black"; ctx.fillText(text, 0, 12);
return ctx.measureText(text).width; // crucial, fast but called too often
};
function measureText_initTable()
{
var css = "padding:0; margin:0; border:none; font:inherit;";
var $table = $('<table style="'+ css +'width:auto;zoom:1;position:absolute;"><tr style="'+ css +'"><td style="'+ css +'white-space:nowrap;"></td></tr></table>');
$td = $("td", $table);
$(this).html( $table );
return $td;
};
// measurement using table
function measureText_table( text, $td )
{
$td.text( text );
return $td.width(); // crucial but expensive
};
$.fn.shorten.defaults = {
tail: "…",
tooltip: true
};
})(jQuery);