Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 espaco 1
(function ($) {
2
  'use strict';
3
  /**
4
   * We need an event when the elements are destroyed
5
   * because if an input is remvoed, 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
          showCharsTyped: true, // show the number of characters typed and not the number of characters remaining
36
          validate: false, // if the browser doesn't support the maxlength attribute, attempt to type more than
37
          // the indicated chars, will be prevented.
38
          utf8: false, // counts using bytesize rather than length. eg: '£' is counted as 2 characters.
39
          appendToParent: false, // append the indicator to the input field's parent instead of body
40
          twoCharLinebreak: true,  // count linebreak as 2 characters to match IE/Chrome textarea validation. As well as DB storage.
41
          allowOverMax: false  // false = use maxlength attribute and browswer functionality.
42
          // true = removes maxlength attribute and replaces with 'data-bs-mxl'.
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(indicator) {
157
        indicator.css({
158
          display: 'block'
159
        });
160
      }
161
 
162
      /**
163
       * When called shows the indicator.
164
       *
165
       * @param indicator
166
       */
167
      function hideRemaining(indicator) {
168
        indicator.css({
169
          display: 'none'
170
        });
171
      }
172
 
173
      /**
174
      * This function updates the value in the indicator
175
      *
176
      * @param maxLengthThisInput
177
      * @param typedChars
178
      * @return String
179
      */
180
      function updateMaxLengthHTML(maxLengthThisInput, typedChars) {
181
        var output = '';
182
        if (options.message) {
183
          output = options.message.replace('%charsTyped%', typedChars)
184
              .replace('%charsRemaining%', maxLengthThisInput - typedChars)
185
              .replace('%charsTotal%', maxLengthThisInput);
186
        } else {
187
          if (options.preText) {
188
            output += options.preText;
189
          }
190
          if (!options.showCharsTyped) {
191
            output += maxLengthThisInput - typedChars;
192
          }
193
          else {
194
            output += typedChars;
195
          }
196
          if (options.showMaxLength) {
197
            output += options.separator + maxLengthThisInput;
198
          }
199
          if (options.postText) {
200
            output += options.postText;
201
          }
202
        }
203
        return output;
204
      }
205
 
206
      /**
207
       * This function updates the value of the counter in the indicator.
208
       * Wants as parameters: the number of remaining chars, the element currently managed,
209
       * the maxLength for the current input and the indicator generated for it.
210
       *
211
       * @param remaining
212
       * @param currentInput
213
       * @param maxLengthCurrentInput
214
       * @param maxLengthIndicator
215
       */
216
      function manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator) {
217
        maxLengthIndicator.html(updateMaxLengthHTML(maxLengthCurrentInput, (maxLengthCurrentInput - remaining)));
218
 
219
        if (remaining > 0) {
220
          if (charsLeftThreshold(currentInput, options.threshold, maxLengthCurrentInput)) {
221
            showRemaining(maxLengthIndicator.removeClass(options.limitReachedClass).addClass(options.warningClass));
222
          } else {
223
            hideRemaining(maxLengthIndicator);
224
          }
225
        } else {
226
          showRemaining(maxLengthIndicator.removeClass(options.warningClass).addClass(options.limitReachedClass));
227
        }
228
 
229
        if (options.allowOverMax) {
230
          // class to use for form validation on custom maxlength attribute
231
          if (remaining < 0) {
232
            currentInput.addClass('overmax');
233
          } else {
234
            currentInput.removeClass('overmax');
235
          }
236
        }
237
      }
238
 
239
      /**
240
       * This function returns an object containing all the
241
       * informations about the position of the current input
242
       *
243
       * @param currentInput
244
       * @return object {bottom height left right top width}
245
       *
246
       */
247
      function getPosition(currentInput) {
248
        var el = currentInput[0];
249
        return $.extend({}, (typeof el.getBoundingClientRect === 'function') ? el.getBoundingClientRect() : {
250
          width: el.offsetWidth,
251
          height: el.offsetHeight
252
        }, currentInput.offset());
253
      }
254
 
255
      /**
256
       * This function places the maxLengthIndicator at the
257
       * top / bottom / left / right of the currentInput
258
       *
259
       * @param currentInput
260
       * @param maxLengthIndicator
261
       * @return null
262
       *
263
       */
264
      function place(currentInput, maxLengthIndicator) {
265
        var pos = getPosition(currentInput),
266
          inputOuter = currentInput.outerWidth(),
267
          outerWidth = maxLengthIndicator.outerWidth(),
268
          actualWidth = maxLengthIndicator.width(),
269
          actualHeight = maxLengthIndicator.height();
270
 
271
        // get the right position if the indicator is appended to the input's parent
272
        if (options.appendToParent) {
273
          pos.top -= currentInput.parent().offset().top;
274
          pos.left -= currentInput.parent().offset().left;
275
        }
276
 
277
        switch (options.placement) {
278
          case 'bottom':
279
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 });
280
            break;
281
          case 'top':
282
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 });
283
            break;
284
          case 'left':
285
            maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth });
286
            break;
287
          case 'right':
288
            maxLengthIndicator.css({ top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width });
289
            break;
290
          case 'bottom-right':
291
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width });
292
            break;
293
          case 'top-right':
294
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter });
295
            break;
296
          case 'top-left':
297
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left - outerWidth });
298
            break;
299
          case 'bottom-left':
300
            maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left - outerWidth });
301
            break;
302
          case 'centered-right':
303
            maxLengthIndicator.css({ top: pos.top + (actualHeight / 2), left: pos.left + inputOuter - outerWidth - 3 });
304
            break;
305
 
306
            // Some more options for placements
307
          case 'bottom-right-inside':
308
            maxLengthIndicator.css({ top: pos.top + pos.height, left: pos.left + pos.width - outerWidth });
309
            break;
310
          case 'top-right-inside':
311
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left + inputOuter - outerWidth });
312
            break;
313
          case 'top-left-inside':
314
            maxLengthIndicator.css({ top: pos.top - actualHeight, left: pos.left });
315
            break;
316
          case 'bottom-left-inside':
317
            maxLengthIndicator.css({ top: pos.top + currentInput.outerHeight(), left: pos.left });
318
            break;
319
        }
320
      }
321
 
322
      /**
323
       * This function retrieves the maximum length of currentInput
324
       *
325
       * @param currentInput
326
       * @return {number}
327
       *
328
       */
329
      function getMaxLength(currentInput) {
330
        var attr = 'maxlength';
331
        if (options.allowOverMax) {
332
          attr = 'data-bs-mxl';
333
        }
334
        return currentInput.attr(attr) || currentInput.attr('size');
335
      }
336
 
337
      return this.each(function () {
338
 
339
        var currentInput = $(this),
340
          maxLengthCurrentInput,
341
          maxLengthIndicator;
342
 
343
        $(window).resize(function () {
344
          if (maxLengthIndicator) {
345
            place(currentInput, maxLengthIndicator);
346
          }
347
        });
348
 
349
        if (options.allowOverMax) {
350
          $(this).attr('data-bs-mxl', $(this).attr('maxlength'));
351
          $(this).removeAttr('maxlength');
352
        }
353
 
354
        function firstInit() {
355
          var maxlengthContent = updateMaxLengthHTML(maxLengthCurrentInput, '0');
356
          maxLengthCurrentInput = getMaxLength(currentInput);
357
 
358
          if (!maxLengthIndicator) {
359
            maxLengthIndicator = $('<span class="bootstrap-maxlength"></span>').css({
360
              display: 'none',
361
              position: 'absolute',
362
              whiteSpace: 'nowrap',
363
              zIndex: 1099
364
            }).html(maxlengthContent);
365
          }
366
 
367
          // We need to detect resizes if we are dealing with a textarea:
368
          if (currentInput.is('textarea')) {
369
            currentInput.data('maxlenghtsizex', currentInput.outerWidth());
370
            currentInput.data('maxlenghtsizey', currentInput.outerHeight());
371
 
372
            currentInput.mouseup(function () {
373
              if (currentInput.outerWidth() !== currentInput.data('maxlenghtsizex') || currentInput.outerHeight() !== currentInput.data('maxlenghtsizey')) {
374
                place(currentInput, maxLengthIndicator);
375
              }
376
 
377
              currentInput.data('maxlenghtsizex', currentInput.outerWidth());
378
              currentInput.data('maxlenghtsizey', currentInput.outerHeight());
379
            });
380
          }
381
 
382
          if (options.appendToParent) {
383
            currentInput.parent().append(maxLengthIndicator);
384
            currentInput.parent().css('position', 'relative');
385
          } else {
386
            documentBody.append(maxLengthIndicator);
387
          }
388
 
389
          var remaining = remainingChars(currentInput, getMaxLength(currentInput));
390
          manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
391
          place(currentInput, maxLengthIndicator);
392
        }
393
 
394
        if (options.showOnReady) {
395
          currentInput.ready(function () {
396
            firstInit();
397
          });
398
        } else {
399
          currentInput.focus(function () {
400
            firstInit();
401
          });
402
        }
403
 
404
        currentInput.on('destroyed', function () {
405
          if (maxLengthIndicator) {
406
            maxLengthIndicator.remove();
407
          }
408
        });
409
 
410
        currentInput.on('blur', function () {
411
          if (maxLengthIndicator && !options.showOnReady) {
412
            maxLengthIndicator.remove();
413
          }
414
        });
415
 
416
        currentInput.on('input', function () {
417
          var maxlength = getMaxLength(currentInput),
418
            remaining = remainingChars(currentInput, maxlength),
419
            output = true;
420
 
421
          if (options.validate && remaining < 0) {
422
            truncateChars(currentInput, maxlength);
423
            output = false;
424
          } else {
425
            manageRemainingVisibility(remaining, currentInput, maxLengthCurrentInput, maxLengthIndicator);
426
          }
427
 
428
          //reposition the indicator if placement "bottom-right-inside" & "top-right-inside" is used
429
          if (options.placement === 'bottom-right-inside' || options.placement === 'top-right-inside') {
430
            place(currentInput, maxLengthIndicator);
431
          }
432
 
433
          return output;
434
        });
435
      });
436
    }
437
  });
438
}(jQuery));