-
Notifications
You must be signed in to change notification settings - Fork 46
/
textcounter.js
358 lines (293 loc) · 16.1 KB
/
textcounter.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
/*!
* jQuery Text Counter Plugin v0.9.1
* https://github.com/ractoon/jQuery-Text-Counter
*
* Copyright 2014 ractoon
* Released under the MIT license
*/
;(function($) {
$.textcounter = function(el, options) {
// To avoid scope issues, use 'base' instead of 'this'
// to reference this class from internal events and functions.
var base = this;
// Access to jQuery and DOM versions of element
base.$el = $(el);
base.el = el;
// Add a reverse reference to the DOM object
base.$el.data('textcounter', base);
base.init = function() {
base.options = $.extend({}, $.textcounter.defaultOptions, options);
// append the count element
var counterText = base.options.countDown ? base.options.countDownText : base.options.counterText,
counterNum = base.options.countDown ? base.options.max : 0,
$formatted_counter_text = $('<div/>').addClass(base.options.textCountMessageClass)
.attr('aria-live', 'polite').attr('aria-atomic', 'true')
.html(counterText.replace('%d', '<span class="' + base.options.textCountClass + '">' + counterNum + '</span>')),
$count_overflow_text = $('<div/>').addClass(base.options.countOverflowContainerClass);
base.hideMessage($count_overflow_text);
base.$container = $('<' + base.options.countContainerElement + '/>')
.addClass(base.options.countContainerClass)
.append($formatted_counter_text)
.append($count_overflow_text);
base.$text_counter = base.$container.find('span');
base.$el.after(base.$container);
// bind input events
base.$el.bind('keyup.textcounter click.textcounter blur.textcounter focus.textcounter change.textcounter paste.textcounter', base.checkLimits).trigger('click.textcounter');
// TextCounter: init(el) Callback
base.options.init(base.el);
};
base.checkLimits = function(e) {
var $this = base.$el,
$countEl = base.$container,
$text = $this.val(),
textCount = 0,
textTotalCount = 0,
eventTriggered = e.originalEvent === undefined ? false : true;
if (!$.isEmptyObject($text)) {
textCount = base.textCount($text);
}
// if max is auto retrieve value
if (base.options.max == 'auto') {
var max = base.$el.attr('maxlength');
if (typeof max !== 'undefined' && max !== false) {
base.options.max = max;
}
else {
base.$container.text('error: [maxlength] attribute not set');
}
}
else if (base.options.max == 'autocustom') {
var max = base.$el.attr(base.options.autoCustomAttr);
if (typeof max !== 'undefined' && max !== false) {
base.options.max = max;
}
else {
base.$container.text('error: [' + base.options.autoCustomAttr + '] attribute not set');
}
}
// if this is a countdown counter deduct from the max characters/words
textTotalCount = base.options.countDown ? base.options.max - textCount : textCount;
// set the current text count
base.setCount(textTotalCount);
if (base.options.min > 0 && eventTriggered) { // if a minimum value has been set
if (textCount < base.options.min) {
base.setErrors('min');
// TextCounter: minunder(el) Callback
base.options.minunder(base.el);
}
else if (textCount >= base.options.min) {
// TextCounter: mincount(el) Callback
base.options.mincount(base.el);
base.clearErrors('min');
}
}
if (base.options.max !== -1) { // if a maximum value has been set
if (textCount === base.options.max && base.options.max !== 0) {
// TextCounter: maxcount(el) Callback
base.options.maxcount(base.el);
base.clearErrors('max');
} else if (textCount > base.options.max && base.options.max !== 0) {
if (base.options.stopInputAtMaximum) { // if the string should be trimmed at the maximum length
var trimmedString = '';
if (base.options.type == "word") { // word type
var wordArray = $text.split(/[^\S\n]/g);
var i = 0;
// iterate over individual words
while (i < wordArray.length) {
// if over the maximum words allowed break;
if (i >= base.options.max) break;
if (wordArray[i] !== undefined) {
trimmedString += wordArray[i] + ' ';
i++;
}
}
}
else { // character type
var maxLimit = (base.options.twoCharCarriageReturn) ?
base.options.max - base.twoCharCarriageReturnCount($text)
: base.options.max;
if (base.options.countSpaces) { // if spaces should be counted
trimmedString = $text.substring(0, maxLimit);
}
else {
var charArray = $text.split(''),
totalCharacters = charArray.length,
charCount = 0,
i = 0;
while (charCount < maxLimit && i < totalCharacters) {
if (charArray[i] !== ' ') charCount++;
trimmedString += charArray[i++];
}
}
}
$this.val(trimmedString.trim());
textCount = base.textCount($this.val());
textTotalCount = base.options.countDown ? base.options.max - textCount : textCount;
base.setCount(textTotalCount);
} else {
base.setErrors('max');
}
}
else {
// TextCounter: maxunder(el) Callback
base.options.maxunder(base.el);
base.clearErrors('max');
}
}
// hide the counter if it doesn't meet either the minimum or maximum display cutoff
if (base.options.minDisplayCutoff == -1 && base.options.maxDisplayCutoff == -1) {
base.$container.show();
} else if (textCount <= base.options.min + base.options.minDisplayCutoff) {
base.$container.show();
} else if (base.options.max !== -1 && textCount >= base.options.max - base.options.maxDisplayCutoff) {
base.$container.show();
} else {
base.$container.hide();
}
};
base.textCount = function(text) {
var textCount = 0;
if (base.options.type == "word") { // word count
textCount = base.wordCount(text);
}
else { // character count
textCount = base.characterCount(text);
}
return textCount;
};
base.wordCount = function(text) {
return text.trim().replace(/\s+/gi, ' ').split(' ').length;
};
base.characterCount = function(text) {
var textCount = 0,
carriageReturnsCount = 0;
// count carriage returns/newlines as 2 characters
if (base.options.twoCharCarriageReturn) {
carriageReturnsCount = base.twoCharCarriageReturnCount(text);
}
if (base.options.countSpaces) { // if need to count spaces
textCount = text.replace(/[^\S\n|\r|\r\n]/g, ' ').length;
}
else {
textCount = text.replace(/\s/g, '').length;
}
// count extended characters (e.g. Chinese)
if (base.options.countExtendedCharacters) {
var extended = text.match(/[^\x00-\xff]/gi);
if (extended == null) {
textCount = text.length;
} else {
textCount = text.length + extended.length;
}
}
if (base.options.twoCharCarriageReturn) {
textCount += carriageReturnsCount;
}
return textCount;
};
base.twoCharCarriageReturnCount = function(text) {
var carriageReturns = text.match(/(\r\n|\n|\r)/g),
carriageReturnsCount = 0;
if (carriageReturns !== null) {
carriageReturnsCount = carriageReturns.length;
}
return carriageReturnsCount;
};
base.setCount = function(count) {
base.$text_counter.text(count);
};
base.setErrors = function(type) {
var $this = base.$el,
$countEl = base.$container,
errorText = '';
$this.addClass(base.options.inputErrorClass);
$countEl.addClass(base.options.counterErrorClass);
switch(type) {
case 'min':
errorText = base.options.minimumErrorText;
break;
case 'max':
errorText = base.options.maximumErrorText;
if (base.options.countOverflow) {
base.setOverflowMessage();
}
break;
}
if (base.options.displayErrorText) {
if (!$countEl.children('.error-text-' + type).length) {
$countEl.append('<' + base.options.errorTextElement + ' class="error-text error-text-' + type + '">' + errorText + '</' + base.options.errorTextElement + '>');
}
}
};
base.setOverflowMessage = function () {
base.hideMessage(base.$container.find('.' + base.options.textCountMessageClass));
base.removeOverflowMessage();
var overflowText = base.options.countOverflowText
.replace('%d', base.textCount(base.$el.val()) - base.options.max)
.replace('%type', base.options.type + 's');
var overflowDiv = base.$container.find('.' + base.options.countOverflowContainerClass).append(overflowText);
base.showMessage(overflowDiv);
},
base.removeOverflowMessage = function () {
base.$container.find('.' + base.options.countOverflowContainerClass).empty();
},
base.showMessage = function ($selector) {
$selector.css('display', 'inline');
},
base.hideMessage = function ($selector) {
$selector.css('display', 'none');
},
base.clearErrors = function(type) {
var $this = base.$el,
$countEl = base.$container;
$countEl.children('.error-text-' + type).remove();
if ($countEl.children('.error-text').length == 0) {
base.removeOverflowMessage();
base.showMessage(base.$container.find('.' + base.options.textCountMessageClass));
$this.removeClass(base.options.inputErrorClass);
$countEl.removeClass(base.options.counterErrorClass);
}
};
// kick it off
base.init();
};
$.textcounter.defaultOptions = {
'type' : "character", // "character" or "word"
'min' : 0, // minimum number of characters/words
'max' : 200, // maximum number of characters/words, -1 for unlimited, 'auto' to use maxlength attribute, 'autocustom' to use a custom attribute for the length (must set "autoCustomAttr")
'autoCustomAttr' : "counterlimit", // custom attribute name with the counter limit if the max is 'autocustom'
'countContainerElement' : "div", // HTML element to wrap the text count in
'countContainerClass' : "text-count-wrapper", // class applied to the countContainerElement
'textCountMessageClass' : "text-count-message", // class applied to the counter message
'textCountClass' : "text-count", // class applied to the counter length (the count number)
'inputErrorClass' : "error", // error class appended to the input element if error occurs
'counterErrorClass' : "error", // error class appended to the countContainerElement if error occurs
'counterText' : "Total Count: %d", // counter text
'errorTextElement' : "div", // error text element
'minimumErrorText' : "Minimum not met", // error message for minimum not met,
'maximumErrorText' : "Maximum exceeded", // error message for maximum range exceeded,
'displayErrorText' : true, // display error text messages for minimum/maximum values
'stopInputAtMaximum' : true, // stop further text input if maximum reached
'countSpaces' : false, // count spaces as character (only for "character" type)
'countDown' : false, // if the counter should deduct from maximum characters/words rather than counting up
'countDownText' : "Remaining: %d", // count down text
'countExtendedCharacters' : false, // count extended UTF-8 characters as 2 bytes (such as Chinese characters)
'twoCharCarriageReturn' : false, // count carriage returns/newlines as 2 characters
'countOverflow' : false, // display text overflow element
'countOverflowText' : "Maximum %type exceeded by %d", // count overflow text
'countOverflowContainerClass' : "text-count-overflow-wrapper", // class applied to the count overflow wrapper
'minDisplayCutoff' : -1, // maximum number of characters/words above the minimum to display a count
'maxDisplayCutoff' : -1, // maximum number of characters/words below the maximum to display a count
// Callback API
'maxunder' : function(el){}, // Callback: function(element) - Fires when counter under max limit
'minunder' : function(el){}, // Callback: function(element) - Fires when counter under min limit
'maxcount' : function(el){}, // Callback: function(element) - Fires when the counter hits the maximum word/character count
'mincount' : function(el){}, // Callback: function(element) - Fires when the counter hits the minimum word/character count
'init' : function(el){} // Callback: function(element) - Fires after the counter is initially setup
};
$.fn.textcounter = function(options) {
return this.each(function() {
new $.textcounter(this, options);
});
};
})(jQuery);