This repository has been archived by the owner on Oct 18, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 61
/
jquery-listnav.js
390 lines (321 loc) · 13.3 KB
/
jquery-listnav.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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/*
* jQuery listnav plugin
*
* Add a slick "letter-based" navigation bar to all of your lists.
* Click a letter to quickly filter the list to items that match that letter.
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Version 3.1.0 (04/26/2021)
* Author: Eric Steinborn
* Compatibility: jQuery 2.0+
*
*/
(function ($) {
$.fn.listnav = function (options) {
var opts = $.extend({}, $.fn.listnav.defaults, options),
firstClick = false,
clickEventType = '';
//detect if you are on a touch device easily.
if (document.ontouchstart !== null) {
clickEventType = 'click';
} else {
// We added 'click' to this becuase on touchscreen computers, when you click with a mouse, it will not fire the click function on a letter, hence disabling the entire plugin functionality. Thanks to my homeboy @clechner77 for pointing this out to me:
// https://github.com/esteinborn/jquery-listnav/issues/34
clickEventType = 'touchend click';
}
opts.prefixes = $.map(opts.prefixes, function (n) {
return n.toLowerCase();
});
return this.each(function () {
var $wrapper, $letters, $letterCount, left, width, count,
id = this.id,
$list = $(this),
counts = {},
allCount = 0, fullCount = 0,
isAll = true,
prevLetter = '';
if ( !$('#' + id + '-nav').length ) {
$('<div id="' + id + '-nav" class="listNav"/>').insertBefore($list);
// Insert the nav if its not been inserted already (preferred method)
// Legacy method was to add the nav yourself in HTML, I didn't like that requirement
}
$wrapper = $('#' + id + '-nav');
// <ul id="myList"> for list and <div id="myList-nav"> for nav wrapper
function init() {
$wrapper.append(createLettersHtml());
$letters = $('.ln-letters', $wrapper).slice(0, 1);
if ( opts.showCounts ) {
$letterCount = $('.ln-letter-count', $wrapper).slice(0, 1);
}
addClasses();
addNoMatchLI();
bindHandlers();
if (opts.flagDisabled) {
addDisabledClass();
}
// remove nav items we don't need
if ( !opts.includeAll ) {
$('.all', $letters).remove();
}
if ( !opts.includeNums ) {
$('._', $letters).remove();
}
if ( !opts.includeOther ) {
$('.-', $letters).remove();
}
if ( opts.removeDisabled ) {
$('.ln-disabled', $letters).remove();
}
$(':last', $letters).addClass('ln-last');
if ( $.cookie && (opts.cookieName !== null) ) {
var cookieLetter = $.cookie(opts.cookieName);
if ( cookieLetter !== null && typeof cookieLetter !== "undefined" ) {
opts.initLetter = cookieLetter;
}
}
// decide what to show first
// Is there an initLetter set, if so, show that letter first
if ( opts.initLetter !== '' ) {
firstClick = true;
// click the initLetter if there was one
$('.' + opts.initLetter.toLowerCase(), $letters).slice(0, 1).trigger('click');
} else {
// If you want to Hide all options until a user clicks
if ( opts.initHidden ) {
addInitHiddenLI();
$list.children().addClass("listNavHide");
$list.children('.ln-init-hidden').removeClass('listNavHide');
}
// If no init letter is set, and you included All, then show it
else if ( opts.includeAll ) {
// make the All link look clicked, but don't actually click it
$('.all', $letters).addClass('ln-selected');
} else {
// All was not included, lets find the first letter with a count and show it
for ( var i = ((opts.includeNums) ? 0 : 1); i < opts.letters.length; i++) {
if ( counts[opts.letters[i]] > 0 ) {
firstClick = true;
$('.' + opts.letters[i], $letters).slice(0, 1).trigger('click');
break;
}
}
}
}
}
// position the letter count above the letter links
function setLetterCountTop() {
// we're going to need to subtract this from the top value of the wrapper to accomodate changes in font-size in CSS.
var letterCountHeight = $letterCount.outerHeight();
$letterCount.css({
top: $('a:first', $wrapper).slice(0, 1).position().top - letterCountHeight
// we're going to grab the first anchor in the list
// We can no longer guarantee that a specific letter will be present
// since adding the "removeDisabled" option
});
}
// adds a class to each LI that has text content inside of it (ie, inside an <a>, a <div>, nested DOM nodes, etc)
function addClasses() {
var str, spl, $this,
firstChar = '',
hasPrefixes = (opts.prefixes.length > 0),
hasFilterSelector = (opts.filterSelector.length > 0);
// Iterate over the list and set a class on each one and use that to filter by
$($list).children().each(function () {
$this = $(this);
// I'm assuming you didn't choose a filterSelector, hopefully saving some cycles
if ( !hasFilterSelector ) {
//Grab the first text content of the LI, we'll use this to filter by
str = $.trim($this.text()).toLowerCase();
} else {
// You set a filterSelector so lets find it and use that to search by instead
str = $.trim($this.find(opts.filterSelector).text()).toLowerCase();
}
// This will run only if there is something to filter by, skipping over images and non-filterable content.
if (str !== '') {
// Apply the non-prefix class to LIs that have prefixed content in them
if (hasPrefixes) {
var prefixes = $.map(opts.prefixes, function(value) {
return value.indexOf(' ') <= 0 ? value + ' ' : value;
});
var matches = $.grep(prefixes, function(value) {
return str.indexOf(value) === 0;
});
if (matches.length > 0) {
var afterMatch = str.toLowerCase().split(matches[0])[1];
if(afterMatch !== null) {
firstChar = $.trim(afterMatch).charAt(0);
} else {
firstChar = str.charAt(0);
}
addLetterClass(firstChar, $this, true);
return;
}
}
// Find the first letter in the LI, including prefixes
firstChar = str.charAt(0);
// Doesn't send true to function, which will ++ the All count on prefixed items
addLetterClass(firstChar, $this);
}
});
}
// Add the appropriate letter class to the current element
function addLetterClass(firstChar, $el, isPrefix) {
// Changed /W/ to /wW/ to allow for non-engligh characters
if ( /\wW/.test(firstChar) ) {
firstChar = '-'; // not A-Z, a-z or 0-9, so considered "other"
}
if ( !isNaN(firstChar) ) {
firstChar = '_'; // use '_' if the first char is a number
}
$el.addClass('ln-' + firstChar);
if ( counts[firstChar] === undefined ) {
counts[firstChar] = 0;
}
counts[firstChar]++;
if (!isPrefix) {
allCount++;
}
}
function addDisabledClass() {
for ( var i = 0; i < opts.letters.length; i++ ) {
if ( counts[opts.letters[i]] === undefined ) {
$('.' + opts.letters[i], $letters).addClass('ln-disabled');
}
}
}
function addNoMatchLI() {
$list.append('<li class="ln-no-match listNavHide">' + opts.noMatchText + '</li>');
}
function addInitHiddenLI() {
$list.append('<li class="ln-init-hidden listNavHide">' + opts.initHiddenText + '</li>');
}
function getLetterCount(el) {
if ($(el).hasClass('all')) {
if (opts.dontCount) {
fullCount = allCount - $list.find(opts.dontCount).length;
} else {
fullCount = allCount;
}
return fullCount;
} else {
el = '.ln-' + $(el).attr('class').split(' ')[0];
if (opts.dontCount) {
count = $list.find(el).not(opts.dontCount).length;
} else {
count = $list.find(el).length;
}
return (count !== undefined) ? count : 0; // some letters may not have a count in the hash
}
}
function bindHandlers() {
if (opts.showCounts) {
// sets the top position of the count div in case something above it on the page has resized
$wrapper.mouseover(function () {
setLetterCountTop();
});
//shows the count above the letter
//
$('.ln-letters a', $wrapper).mouseover(function () {
left = $(this).position().left;
width = ($(this).outerWidth()) + 'px';
count = getLetterCount(this);
$letterCount.css({
left: left,
width: width
}).text(count).addClass("letterCountShow").removeClass("listNavHide"); // set left position and width of letter count, set count text and show it
}).mouseout(function () { // mouseout for each letter: hide the count
$letterCount.addClass("listNavHide").removeClass("letterCountShow");
});
}
// click handler for letters: shows/hides relevant LI's
//
$('a', $letters).on(clickEventType, function (e) {
e.preventDefault();
var $this = $(this),
letter = $this.attr('class').split(' ')[0],
noMatches = $list.children('.ln-no-match');
if ( opts.initHidden ) {
$list.children('.ln-init-hidden').remove();
}
if ( prevLetter !== letter ) {
// Only to run this once for each click, won't double up if they clicked the same letter
// Won't hinder firstRun
$('a.ln-selected', $letters).removeClass('ln-selected');
if ( letter === 'all' ) {
// If ALL button is clicked:
$list.children().addClass("listNavShow").removeClass("listNavHide"); // Show ALL
noMatches.addClass("listNavHide").removeClass("listNavShow"); // Hide the list item for no matches
isAll = true; // set this to quickly check later
} else {
// If you didn't click ALL
if ( isAll ) {
// since you clicked ALL last time:
$list.children().addClass("listNavHide").removeClass("listNavShow");
isAll = false;
} else if (prevLetter !== '') {
$list.children('.ln-' + prevLetter).addClass("listNavHide").removeClass("listNavShow");
}
var count = getLetterCount(this);
if (count > 0) {
$list.children('.ln-' + letter).addClass("listNavShow").removeClass("listNavHide");
noMatches.addClass("listNavHide").removeClass("listNavShow"); // in case it's showing
} else {
noMatches.addClass("listNavShow").removeClass("listNavHide");
}
}
prevLetter = letter;
if ($.cookie && (opts.cookieName !== null)) {
$.cookie(opts.cookieName, letter, {
expires: 999
});
}
$this.addClass('ln-selected');
$this.blur();
if (!firstClick && (opts.onClick !== null)) {
opts.onClick(letter);
} else {
firstClick = false; //return false;
}
} // end if prevLetter !== letter
}); // end click()
} // end BindHandlers()
// creates the HTML for the letter links
//
function createLettersHtml() {
var html = [];
for (var i = 1; i < opts.letters.length; i++) {
if (html.length === 0) {
html.push('<a class="all" href="#">'+ opts.allText + '</a><a class="_" href="#">0-9</a>');
}
html.push('<a class="' + opts.letters[i] + '" href="#">' + ((opts.letters[i] === '-') ? '...' : opts.letters[i].toUpperCase()) + '</a>');
}
return '<div class="ln-letters">' + html.join('') + '</div>' + ((opts.showCounts) ? '<div class="ln-letter-count listNavHide">0</div>' : '');
// Remove inline styles, replace with css class
// Element will be repositioned when made visible
}
init();
});
};
$.fn.listnav.defaults = {
allText: 'All',
cookieName: null,
dontCount: '',
filterSelector: '',
flagDisabled: true,
includeAll: true,
includeNums: true,
includeOther: false,
initHidden: false,
initHiddenText: 'Tap a letter above to view matching items',
initLetter: '',
letters: ['_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-'],
noMatchText: 'No matching entries',
onClick: null,
prefixes: [],
removeDisabled: false,
showCounts: true,
};
})(jQuery);