Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
776 lars 1
(function ($) {
2
  'use strict';
3
  /**
4
   * We need an event when the elements are destroyed
5
   * because if an input is removed, we have to remove the
6
   * maxlength object associated (if any).
7
   * From:
8
   * http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
9
   */
10
  if (!$.event.special.destroyed) {
11
    $.event.special.destroyed = {
12
      remove: function (o) {
13
        if (o.handler) {
14
          o.handler();
15
        }
16
      }
17
    };
18
  }
19
 
20
 
21
  $.fn.extend({
22
    maxlength: function (options, callback) {
23
      var documentBody = $('body'),
24
        defaults = {
25
          showOnReady: false, // true to always show when indicator is ready
26
          alwaysShow: false, // if true the indicator it's always shown.
27
          threshold: 10, // Represents how many chars left are needed to show up the counter
28
          warningClass: 'label label-success',
29
          limitReachedClass: 'label label-important label-danger',
30
          separator: ' / ',
31
          preText: '',
32
          postText: '',
33
          showMaxLength: true,
34
          placement: 'bottom',
35
          message: null, // an alternative way to provide the message text
36
          showCharsTyped: true, // show the number of characters typed and not the number of characters remaining
37
          validate: false, // if the browser doesn't support the maxlength attribute, attempt to type more than
38
          // the indicated chars, will be prevented.
39
          utf8: false, // counts using bytesize rather than length. eg: '£' is counted as 2 characters.
40
          appendToParent: false, // append the indicator to the input field's parent instead of body
41
          twoCharLinebreak: true,  // count linebreak as 2 characters to match IE/Chrome textarea validation. As well as DB storage.
42
          customMaxAttribute: null  // null = use maxlength attribute and browser functionality, string = use specified attribute instead.
43
          // Form submit validation is handled on your own.  when maxlength has been exceeded 'overmax' class added to element
44
        };
45
 
46
      if ($.isFunction(options) && !callback) {
47
        callback = options;
48
        options = {};
49
      }
50
      options = $.extend(defaults, options);
51
 
52
      /**
53
      * Return the length of the specified input.
54
      *
55
      * @param input
56
      * @return {number}
57
      */
58
      function inputLength(input) {
59
        var text = input.val();
60
 
61
        if (options.twoCharLinebreak) {
62
          // Count all line breaks as 2 characters
63
          text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
64
        } else {
65
          // Remove all double-character (\r\n) linebreaks, so they're counted only once.
66
          text = text.replace(new RegExp('\r?\n', 'g'), '\n');
67
        }
68
 
69
        var currentLength = 0;
70
 
71
        if (options.utf8) {
72
          currentLength = utf8Length(text);
73
        } else {
74
          currentLength = text.length;
75
        }
76
        return currentLength;
77
      }
78
 
79
      /**
80
      * Truncate the text of the specified input.
81
      *
82
      * @param input
83
      * @param limit
84
      */
85
      function truncateChars(input, maxlength) {
86
        var text = input.val();
87
        var newlines = 0;
88
 
89
        if (options.twoCharLinebreak) {
90
          text = text.replace(/\r(?!\n)|\n(?!\r)/g, '\r\n');
91
 
92
          if (text.substr(text.length - 1) === '\n' && text.length % 2 === 1) {
93
            newlines = 1;
94
          }
95
        }
96
 
97
        input.val(text.substr(0, maxlength - newlines));
98
      }
99
 
100
      /**
101
      * Return the length of the specified input in UTF8 encoding.
102
      *
103
      * @param input
104
      * @return {number}
105
      */
106
      function utf8Length(string) {
107
        var utf8length = 0;
108
        for (var n = 0; n < string.length; n++) {
109
          var c = string.charCodeAt(n);
110
          if (c < 128) {
111
            utf8length++;
112
          }
113
          else if ((c > 127) && (c < 2048)) {
114
            utf8length = utf8length + 2;
115
          }
116
          else {
117
            utf8length = utf8length + 3;
118
          }
119
        }
120
        return utf8length;
121
      }
122
 
123
      /**
124
       * Return true if the indicator should be showing up.
125
       *
126
       * @param input
127
       * @param thereshold
128
       * @param maxlength
129
       * @return {number}
130
       */
131
      function charsLeftThreshold(input, thereshold, maxlength) {
132
        var output = true;
133
        if (!options.alwaysShow && (maxlength - inputLength(input) > thereshold)) {
134
          output = false;
135
        }
136
        return output;
137
      }
138
 
139
      /**
140
       * Returns how many chars are left to complete the fill up of the form.
141
       *
142
       * @param input
143
       * @param maxlength
144
       * @return {number}
145
       */
146
      function remainingChars(input, maxlength) {
147
        var length = maxlength - inputLength(input);
148
        return length;
149
      }
150
 
151
      /**
152
       * When called displays the indicator.
153
       *
154
       * @param indicator
155
       */
156
      function showRemaining(currentInput, indicator) {
157
        indicator.css({
158
          display: 'block'
159
        });
160
        currentInput.trigger('maxlength.shown');
161
      }
162
 
163
      /**
164
       * When called shows the indicator.
165
       *
166
       * @param indicator
167
       */
168
      function hideRemaining(currentInput, indicator) {
169
        indicator.css({
170
          display: 'none'
171
        });
172
        currentInput.trigger('maxlength.hidden');
173
      }
174
 
175
      /**
176
      * This function updates the value in the indicator
177
      *
178
      * @param maxLengthThisInput
179
      * @param typedChars
180
      * @return String
181
      */
182
      function updateMaxLengthHTML(currentInputText, maxLengthThisInput, typedChars) {
183
        var output = '';
184
        if (options.message) {
185
          if (typeof options.message === 'function') {
186
            output = options.message(currentInputText, maxLengthThisInput);
187
          } else {
188
            output = options.message.replace('%charsTyped%', typedChars)
189
              .replace('%charsRemaining%', maxLengthThisInput - typedChars)
190
              .replace('%charsTotal%', maxLengthThisInput);
191
          }
192
        } else {
193
          if (options.preText) {
194
            output += options.preText;
195
          }
196
          if (!options.showCharsTyped) {
197
            output += maxLengthThisInput - typedChars;
198
          }
199
          else {
200
            output += typedChars;
201
          }
202
          if (options.showMaxLength) {
203
            output += options.separator + maxLengthThisInput;
204
          }
205
          if (options.postText) {
206
            output += options.postText;
207
          }
208
        }
209
        return output;
210
      }
211
 
212
      /**
213
       * This function updates the value of the counter in the indicator.
214
       * Wants as parameters: the number of remaining chars, the element currently managed,
215
       * the maxLength for the current input and the indicator generated for it.
216
       *
217
       * @param remaining
218
       * @param currentInput
219
       * @param maxLengthCurrentInput
220
       * @param maxLengthIndicator
221
       */
222
      function manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator) {
223
        if (maxLengthIndicator) {
224
          maxLengthIndicator.html(updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, (maxLengthCurrentInput - remaining)));
225
 
226
          if (remaining > 0) {
227
            if (charsLeftThreshold(currentInput, options.threshold, maxLengthCurrentInput)) {
228
              showRemaining(currentInput, maxLengthIndicator.removeClass(options.limitReachedClass).addClass(options.warningClass));
229
            } else {
230
              hideRemaining(currentInput, maxLengthIndicator);
231
            }
232
          } else {
233
            showRemaining(currentInput, maxLengthIndicator.removeClass(options.warningClass).addClass(options.limitReachedClass));
234
          }
235
        }
236
 
237
        if (options.customMaxAttribute) {
238
          // class to use for form validation on custom maxlength attribute
239
          if (remaining < 0) {
240
            currentInput.addClass('overmax');
241
          } else {
242
            currentInput.removeClass('overmax');
243
          }
244
        }
245
      }
246
 
247
      /**
248
       * This function returns an object containing all the
249
       * informations about the position of the current input
250
       *
251
       * @param currentInput
252
       * @return object {bottom height left right top width}
253
       *
254
       */
255
      function getPosition(currentInput) {
256
        var el = currentInput[0];
257
        return $.extend({}, (typeof el.getBoundingClientRect === 'function') ? el.getBoundingClientRect() : {
258
          width: el.offsetWidth,
259
          height: el.offsetHeight
260
        }, currentInput.offset());
261
      }
262
 
263
      /**
264
       * This function places the maxLengthIndicator at the
265
       * top / bottom / left / right of the currentInput
266
       *
267
       * @param currentInput
268
       * @param maxLengthIndicator
269
       * @return null
270
       *
271
       */
272
      function place(currentInput, maxLengthIndicator) {
273
        var pos = getPosition(currentInput);
274
 
275
        // Supports custom placement handler
276
        if ($.type(options.placement) === 'function'){
277
          options.placement(currentInput, maxLengthIndicator, pos);
278
          return;
279
        }
280
 
281
        // Supports custom placement via css positional properties
282
        if ($.isPlainObject(options.placement)){
283
          placeWithCSS(options.placement, maxLengthIndicator);
284
          return;
285
        }
286
 
287
        var inputOuter = currentInput.outerWidth(),
288
          outerWidth = maxLengthIndicator.outerWidth(),
289
          actualWidth = maxLengthIndicator.width(),
290
          actualHeight = maxLengthIndicator.height();
291
 
292
        // get the right position if the indicator is appended to the input's parent
293
        if (options.appendToParent) {
294
          pos.top -= currentInput.parent().offset().top;
295
          pos.left -= currentInput.parent().offset().left;
296
        }
297
 
298
        switch (options.placement) {
299
          case 'bottom':
300
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 });
301
            break;
302
          case 'top':
303
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 });
304
            break;
305
          case 'left':
306
            maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth });
307
            break;
308
          case 'right':
309
            maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width });
310
            break;
311
          case 'bottom-right':
312
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width });
313
            break;
314
          case 'top-right':
315
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter });
316
            break;
317
          case 'top-left':
318
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left - outerWidth });
319
            break;
320
          case 'bottom-left':
321
            maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left - outerWidth });
322
            break;
323
          case 'centered-right':
324
            maxLengthIndicator.css({ top: pos.top + (actualHeight / 2), left: pos.left + inputOuter - outerWidth - 3 });
325
            break;
326
 
327
            // Some more options for placements
328
          case 'bottom-right-inside':
329
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width - outerWidth });
330
            break;
331
          case 'top-right-inside':
332
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter - outerWidth });
333
            break;
334
          case 'top-left-inside':
335
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left });
336
            break;
337
          case 'bottom-left-inside':
338
            maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left });
339
            break;
340
        }
341
      }
342
 
343
      /**
344
       * This function places the maxLengthIndicator based on placement config object.
345
       *
346
       * @param {object} placement
347
       * @param {$} maxLengthIndicator
348
       * @return null
349
       *
350
       */
351
      function placeWithCSS(placement, maxLengthIndicator) {
352
        if (!placement || !maxLengthIndicator){
353
          return;
354
        }
355
 
356
        var POSITION_KEYS = [
357
          'top',
358
          'bottom',
359
          'left',
360
          'right',
361
          'position'
362
        ];
363
 
364
        var cssPos = {};
365
 
366
        // filter css properties to position
367
        $.each(POSITION_KEYS, function (i, key) {
368
          var val = options.placement[key];
369
          if (typeof val !== 'undefined'){
370
            cssPos[key] = val;
371
          }
372
        });
373
 
374
        maxLengthIndicator.css(cssPos);
375
 
376
        return;
377
      }
378
 
379
      /**
380
       * This function returns true if the indicator position needs to
381
       * be recalculated when the currentInput changes
382
       *
383
       * @return {boolean}
384
       *
385
       */
386
      function isPlacementMutable() {
387
        return options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside' || typeof options.placement === 'function' || (options.message && typeof options.message === 'function');
388
      }
389
 
390
      /**
391
       * This function retrieves the maximum length of currentInput
392
       *
393
       * @param currentInput
394
       * @return {number}
395
       *
396
       */
397
      function getMaxLength(currentInput) {
398
        var max = currentInput.attr('maxlength');
399
 
400
        if (options.customMaxAttribute) {
401
          var custom = currentInput.attr(options.customMaxAttribute);
402
          if (!max || custom < max) {
403
            max = custom;
404
          }
405
        }
406
 
407
        if (!max) {
408
          max = currentInput.attr('size');
409
        }
410
        return max;
411
      }
412
 
413
      return this.each(function () {
414
 
415
        var currentInput = $(this),
416
          maxLengthCurrentInput,
417
          maxLengthIndicator;
418
 
419
        $(window).resize(function () {
420
          if (maxLengthIndicator) {
421
            place(currentInput, maxLengthIndicator);
422
          }
423
        });
424
 
425
        function firstInit() {
426
          var maxlengthContent = updateMaxLengthHTML(currentInput.val(), maxLengthCurrentInput, '0');
427
          maxLengthCurrentInput = getMaxLength(currentInput);
428
 
429
          if (!maxLengthIndicator) {
430
            maxLengthIndicator = $('<span class="bootstrap-maxlength"></span>').css({
431
              display: 'none',
432
              position: 'absolute',
433
              whiteSpace: 'nowrap',
434
              zIndex: 1099
435
            }).html(maxlengthContent);
436
          }
437
 
438
          // We need to detect resizes if we are dealing with a textarea:
439
          if (currentInput.is('textarea')) {
440
            currentInput.data('maxlenghtsizex', currentInput.outerWidth());
441
            currentInput.data('maxlenghtsizey', currentInput.outerHeight());
442
 
443
            currentInput.mouseup(function () {
444
              if (currentInput.outerWidth() !== currentInput.data('maxlenghtsizex') || currentInput.outerHeight() !== currentInput.data('maxlenghtsizey')) {
445
                place(currentInput, maxLengthIndicator);
446
              }
447
 
448
              currentInput.data('maxlenghtsizex', currentInput.outerWidth());
449
              currentInput.data('maxlenghtsizey', currentInput.outerHeight());
450
            });
451
          }
452
 
453
          if (options.appendToParent) {
454
            currentInput.parent().append(maxLengthIndicator);
455
            currentInput.parent().css('position', 'relative');
456
          } else {
457
            documentBody.append(maxLengthIndicator);
458
          }
459
 
460
          var remaining = remainingChars(currentInput, getMaxLength(currentInput));
461
          manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
462
          place(currentInput, maxLengthIndicator);
463
        }
464
 
465
        if (options.showOnReady) {
466
          currentInput.ready(function () {
467
            firstInit();
468
          });
469
        } else {
470
          currentInput.focus(function () {
471
            firstInit();
472
          });
473
        }
474
 
475
        currentInput.on('maxlength.reposition', function () {
476
          place(currentInput, maxLengthIndicator);
477
        });
478
 
479
 
480
        currentInput.on('destroyed', function () {
481
          if (maxLengthIndicator) {
482
            maxLengthIndicator.remove();
483
          }
484
        });
485
 
486
        currentInput.on('blur', function () {
487
          if (maxLengthIndicator && !options.showOnReady) {
488
            maxLengthIndicator.remove();
489
          }
490
        });
491
 
492
        currentInput.on('input', function () {
493
          var maxlength = getMaxLength(currentInput),
494
            remaining = remainingChars(currentInput, maxlength),
495
            output = true;
496
 
497
          if (options.validate && remaining < 0) {
498
            truncateChars(currentInput, maxlength);
499
            output = false;
500
          } else {
501
            manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
502
          }
503
 
504
          if (isPlacementMutable()) {
505
            place(currentInput, maxLengthIndicator);
506
          }
507
 
508
          return output;
509
        });
510
      });
511
    }
512
  });
513
}(jQuery));