/
jquery.expander.js
293 lines (246 loc) · 9.4 KB
/
jquery.expander.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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/*!
* jQuery Expander Plugin v1.0pre
*
* Date: Sun Sep 11 17:22:10 2011 EDT
* Requires: jQuery v1.3+
*
* Copyright 2011, Karl Swedberg
* Dual licensed under the MIT and GPL licenses (just like jQuery):
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*
*
*
*/
(function($) {
$.expander = {
version: '1.0pre',
defaults: {
// the number of characters at which the contents will be sliced into two parts.
slicePoint: 100,
// whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
preserveWords: true,
// widow: a threshold of sorts for whether to initially hide/collapse part of the element's contents.
// If after slicing the contents in two there are fewer words in the second part than
// the value set by widow, we won't bother hiding/collapsing anything.
widow: 4,
// text displayed in a link instead of the hidden part of the element.
// clicking this will expand/show the hidden/collapsed text
expandText: 'read more',
expandPrefix: '… ',
// class names for summary element and detail element
summaryClass: 'summary',
detailClass: 'details',
// class names for read-more <span> around "read-more" link and "read-less" link
moreClass: 'read-more',
lessClass: 'read-less',
// number of milliseconds after text has been expanded at which to collapse the text again.
// when 0, no auto-collapsing
collapseTimer: 0,
expandEffect: 'fadeIn',
expandSpeed: 250,
// allow the user to re-collapse the expanded text.
userCollapse: true,
// text to use for the link to re-collapse the text
userCollapseText: 'read less',
userCollapsePrefix: ' ',
// all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called
onSlice: null, // function() {}
beforeExpand: null, // function() {},
afterExpand: null, // function() {},
onCollapse: null // function(byUser) {}
}
};
$.fn.expander = function(options) {
var opts = $.extend({}, $.expander.defaults, options),
rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
rAmpWordEnd = /(&(?:[^;]+;)?|\w+)$/,
rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
rOpenTag = /<(\w+)[^>]*>/g,
rCloseTag = /<\/(\w+)>/g,
rTagPlus = /^<[^>]+>.?/,
delayedCollapse;
this.each(function() {
var i, l, tmp, summTagLess, summOpens, summCloses, lastCloseTag, detailText,
$thisDetails, $readMore,
openTagsForDetails = [],
closeTagsForsummaryText = [],
defined = {},
thisEl = this,
$this = $(this),
$summEl = $([]),
o = $.meta ? $.extend({}, opts, $this.data()) : opts,
hasBlocks = !!$this.find('*').filter(function() {
var display = $(this).css('display');
return (/^block|table|list/).test(display);
}).length,
el = hasBlocks ? 'div' : 'span',
detailSelector = el + '.' + o.detailClass,
moreSelector = 'span.' + o.moreClass,
expandSpeed = o.expandSpeed || 0,
allHtml = $.trim( $this.html() ),
allText = $.trim( $this.text() ),
summaryText = allHtml.slice(0, o.slicePoint);
if (allText.length <= o.slicePoint) {
return;
}
// determine which callback functions are defined
$.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) {
defined[val] = $.isFunction(o[val]);
});
summaryText = backup(summaryText);
// summary text sans tags length
summTagless = summaryText.replace(rOpenCloseTag,'').length;
// add more characters to the summary, one for each character in the tags
while (summTagless < o.slicePoint) {
newChar = allHtml.charAt(summaryText.length);
if (newChar == '<') {
newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
}
summaryText += newChar;
summTagless++;
}
// back up if we're in the middle of a tag or word
summaryText = backup(summaryText, o.preserveWords);
// separate open tags from close tags and clean up the lists
summOpens = summaryText.match(rOpenTag) || [];
summCloses = summaryText.match(rCloseTag) || [];
// filter out self-closing tags
tmp = [];
$.each(summOpens, function(index, val) {
if ( !rSelfClose.test(val) ) {
tmp.push(val);
}
});
summOpens = tmp;
// strip close tags to just the tag name
l = summCloses.length;
for (i = 0; i < l; i++) {
summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
}
// tags that start in summary and end in detail need:
// a). close tag at end of summary
// b). open tag at beginning of detail
$.each(summOpens, function(index, val) {
var thisTagName = val.replace(rOpenTag, '$1');
var closePosition = $.inArray(thisTagName, summCloses);
if (closePosition === -1) {
openTagsForDetails.push(val);
closeTagsForsummaryText.push('</' + thisTagName + '>');
} else {
summCloses.splice(closePosition, 1);
}
});
// reverse the order of the close tags for the summary so they line up right
closeTagsForsummaryText.reverse();
// create necessary expand/collapse elements if they don't already exist
if (!$this.find(detailSelector).length) {
// end script if detail has fewer words than widow option
detailText = allHtml.slice(summaryText.length);
if ( detailText.split(/\s+/).length < o.widow ) {
return;
}
// otherwise, continue...
lastCloseTag = closeTagsForsummaryText.pop() || '';
summaryText += closeTagsForsummaryText.join('');
detailText = openTagsForDetails.join('') + detailText;
o.moreLabel = '<span class="' + o.moreClass + '">' + o.expandPrefix;
o.moreLabel += '<a href="#">' + o.expandText + '</a></span>';
if (hasBlocks) {
summaryText = '<div class="' + o.summaryClass + '">' + summaryText + o.moreLabel;
summaryText += lastCloseTag + '</div>';
detailText = allHtml;
o.expandPrefix = '';
} else {
summaryText += lastCloseTag;
}
// onSlice callback
o.summary = summaryText;
o.details = detailText;
if (defined.onSlice) {
tmp = o.onSlice.call(thisEl, o);
o = tmp && tmp.details ? tmp : o;
}
// build the html with summary and detail and use it to replace old contents
var html = buildHTML(o, hasBlocks);
$this.html( html );
}
// set up details and summary for expanding/collapsing
$thisDetails = $this.find(detailSelector);
$readMore = $this.find(moreSelector);
$thisDetails.hide();
$readMore.find('a').bind('click.expander', expand);
$summEl = $this.find('div.' + o.summaryClass);
if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) {
$this
.find(detailSelector)
.append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>');
$this
.find('span.' + o.lessClass + ' a')
.bind('click.expander', function(event) {
event.preventDefault();
clearTimeout(delayedCollapse);
var $detailsCollapsed = $(this).closest(detailSelector);
reCollapse(o, $detailsCollapsed);
if (defined.onCollapse) {
o.onCollapse.call(thisEl, true);
}
});
}
function expand(event) {
event.preventDefault();
$readMore.hide();
$summEl.hide();
if (defined.beforeExpand) {
o.beforeExpand.call(thisEl);
}
$thisDetails[o.expandEffect](expandSpeed, function() {
$thisDetails.css({zoom: ''});
if (defined.afterExpand) {o.afterExpand.call(thisEl);}
delayCollapse(o, $thisDetails, thisEl);
});
}
}); // this.each
function buildHTML(o, blocks) {
var el = blocks ? 'div' : 'span';
return [
o.summary,
blocks ? '' : o.moreLabel,
'<' + el,
' class="' + o.detailClass + '">',
o.details,
'</' + el + '>'
].join('');
}
function backup(txt, preserveWords) {
if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) {
txt = txt.slice( 0, txt.lastIndexOf('<') );
}
if (preserveWords) {
txt = txt.replace(rAmpWordEnd,'');
}
return txt;
}
function reCollapse(o, el) {
el.hide();
var prevMore = el.prev('span.' + o.moreClass).show();
if (!prevMore.length) {
el.parent().children('div.' + o.summaryClass).show().find('span.' + o.moreClass).show();
}
}
function delayCollapse(option, $collapseEl, thisEl) {
if (option.collapseTimer) {
delayedCollapse = setTimeout(function() {
reCollapse(option, $collapseEl);
if ( $.isFunction(option.onCollapse) ) {
option.onCollapse.call(thisEl, false);
}
}, option.collapseTimer);
}
}
return this;
};
// plugin defaults
$.fn.expander.defaults = $.expander.defaults;
})(jQuery);