source: branches/2.5/prototype/plugins/jquery.keyboard/jquery.keypad.js @ 8232

Revision 8232, 35.7 KB checked in by douglas, 11 years ago (diff)

Ticket #0000 - Copiadas as alterações do Trunk. Versão final 2.5.1.

  • Property svn:executable set to *
Line 
1/* http://keith-wood.name/keypad.html
2   Keypad field entry extension for jQuery v1.5.0.
3   Written by Keith Wood (kbwood{at}iinet.com.au) August 2008.
4   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
5   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
6   Please attribute the author if you use it. */
7   
8(function($) { // hide the namespace
9
10/* Keypad manager.
11   Use the singleton instance of this class, $.keypad, to interact with the plugin.
12   Settings for keypad fields are maintained in instance objects,
13   allowing multiple different settings on the same page. */
14function Keypad() {
15        this._curInst = null; // The current instance in use
16        this._disabledFields = []; // List of keypad fields that have been disabled
17        this._keypadShowing = false; // True if the popup panel is showing , false if not
18        this._keyCode = 0;
19        this._specialKeys = [];
20        this.addKeyDef('CLOSE', 'close', function(inst) {
21                plugin._curInst = (inst._inline ? inst : plugin._curInst);
22                plugin._hidePlugin();
23        });
24        this.addKeyDef('CLEAR', 'clear', function(inst) { plugin._clearValue(inst); });
25        this.addKeyDef('BACK', 'back', function(inst) { plugin._backValue(inst); });
26        this.addKeyDef('SHIFT', 'shift', function(inst) { plugin._shiftKeypad(inst); });
27        this.addKeyDef('SPACE_BAR', 'spacebar', function(inst) { plugin._selectValue(inst, ' '); }, true);
28        this.addKeyDef('SPACE', 'space');
29        this.addKeyDef('HALF_SPACE', 'half-space');
30        this.addKeyDef('ENTER', 'enter', function(inst) { plugin._selectValue(inst, '\x0D'); }, true);
31        this.addKeyDef('TAB', 'tab', function(inst) { plugin._selectValue(inst, '\x09'); }, true);
32        // Standard US keyboard alphabetic layout
33        this.qwertyAlphabetic = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
34        // Standard US keyboard layout
35        this.qwertyLayout = ['!@#$%^&*()_=' + this.HALF_SPACE + this.SPACE + this.CLOSE,
36                this.HALF_SPACE + '`~[]{}<>\\|/' + this.SPACE + '789',
37                'qwertyuiop\'"' + this.HALF_SPACE + '456',
38                this.HALF_SPACE + 'asdfghjkl;:' + this.SPACE + '123',
39                this.SPACE + 'zxcvbnm,.?' + this.SPACE + this.HALF_SPACE + '-0+',
40                '' + this.TAB + this.ENTER + this.SPACE_BAR + this.SHIFT +
41                this.HALF_SPACE + this.BACK + this.CLEAR];
42        this.regional = []; // Available regional settings, indexed by language code
43        this.regional[''] = { // Default regional settings
44                buttonText: '...', // Display text for trigger button
45                buttonStatus: 'Open the keypad', // Status text for trigger button
46                closeText: 'Close', // Display text for close link
47                closeStatus: 'Close the keypad', // Status text for close link
48                clearText: 'Clear', // Display text for clear link
49                clearStatus: 'Erase all the text', // Status text for clear link
50                backText: 'Back', // Display text for back link
51                backStatus: 'Erase the previous character', // Status text for back link
52                spacebarText: '&nbsp;', // Display text for space bar
53                spacebarStatus: 'Space', // Status text for space bar
54                enterText: 'Enter', // Display text for carriage return
55                enterStatus: 'Carriage return', // Status text for carriage return
56                tabText: '→', // Display text for tab
57                tabStatus: 'Horizontal tab', // Status text for tab
58                shiftText: 'Shift', // Display text for shift link
59                shiftStatus: 'Toggle upper/lower case characters', // Status text for shift link
60                alphabeticLayout: this.qwertyAlphabetic, // Default layout for alphabetic characters
61                fullLayout: this.qwertyLayout, // Default layout for full keyboard
62                isAlphabetic: this.isAlphabetic, // Function to determine if character is alphabetic
63                isNumeric: this.isNumeric, // Function to determine if character is numeric
64                toUpper: this.toUpper, // Function to convert characters to upper case
65                isRTL: false // True if right-to-left language, false if left-to-right
66        };
67        this._defaults = { // Global defaults for all the keypad instances
68                showOn: 'focus', // 'focus' for popup on focus,
69                        // 'button' for trigger button, or 'both' for either
70                buttonImage: '', // URL for trigger button image
71                buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
72                showAnim: 'show', // Name of jQuery animation for popup
73                showOptions: {}, // Options for enhanced animations
74                duration: 'normal', // Duration of display/closure
75                appendText: '', // Display text following the text field, e.g. showing the format
76                useThemeRoller: false, // True to add ThemeRoller classes
77                keypadClass: '', // Additional CSS class for the keypad for an instance
78                prompt: '', // Display text at the top of the keypad
79                layout: ['123' + this.CLOSE, '456' + this.CLEAR, '789' + this.BACK, this.SPACE + '0'], // Layout of keys
80                separator: '', // Separator character between keys
81                target: null, // Input target for an inline keypad
82                keypadOnly: true, // True for entry only via the keypad, false for real keyboard too
83                randomiseAlphabetic: false, // True to randomise the alphabetic key positions, false to keep in order
84                randomiseNumeric: false, // True to randomise the numeric key positions, false to keep in order
85                randomiseOther: false, // True to randomise the other key positions, false to keep in order
86                randomiseAll: false, // True to randomise all key positions, false to keep in order
87                beforeShow: null, // Callback before showing the keypad
88                onKeypress: null, // Callback when a key is selected
89                onClose: null // Callback when the panel is closed
90        };
91        $.extend(this._defaults, this.regional['']);
92        this.mainDiv = $('<div class="' + this._mainDivClass + '" style="display: none;"></div>');
93}
94
95$.extend(Keypad.prototype, {
96        /* Class name added to elements to indicate already configured with keypad. */
97        markerClassName: 'hasKeypad',
98        /* Name of the data property for instance settings. */
99        propertyName: 'keypad',
100
101        _mainDivClass: 'keypad-popup', // The main keypad division class
102        _inlineClass: 'keypad-inline', // The inline marker class
103        _appendClass: 'keypad-append', // The append marker class
104        _triggerClass: 'keypad-trigger', // The trigger marker class
105        _disableClass: 'keypad-disabled', // The disabled covering marker class
106        _inlineEntryClass: 'keypad-keyentry', // The inline entry marker class
107        _coverClass: 'keypad-cover', // The IE select cover marker class
108        _rtlClass: 'keypad-rtl', // The right-to-left marker class
109        _rowClass: 'keypad-row', // The keypad row marker class
110        _promptClass: 'keypad-prompt', // The prompt marker class
111        _specialClass: 'keypad-special', // The special key marker class
112        _namePrefixClass: 'keypad-', // The key name marker class prefix
113        _keyClass: 'keypad-key', // The key marker class
114        _keyDownClass: 'keypad-key-down', // The key down marker class
115
116        /* Override the default settings for all keypad instances.
117           @param  settings  (object) the new settings to use as defaults
118           @return  (Keypad) this object */
119        setDefaults: function(settings) {
120                $.extend(this._defaults, settings || {});
121                return this;
122        },
123
124        /* Add the definition of a special key.
125           @param  id           (string) the identifier for this key - access via $.keypad.<id>
126           @param  name         (string) the prefix for localisation strings and
127                                the suffix for a class name
128           @param  action       (function) the action performed for this key -
129                                receives inst as a parameter
130           @param  noHighlight  (boolean) true to suppress highlight when using ThemeRoller
131           @return  (Keypad) this object */
132        addKeyDef: function(id, name, action, noHighlight) {
133                if (this._keyCode == 32) {
134                        throw 'Only 32 special keys allowed';
135                }
136                this[id] = String.fromCharCode(this._keyCode++);
137                this._specialKeys.push({code: this[id], id: id, name: name,
138                        action: action, noHighlight: noHighlight});
139                return this;
140        },
141
142        /* Attach the keypad to a jQuery selection.
143           @param  target   (element) the control to affect
144           @param  options  (object) the custom options for this instance */
145        _attachPlugin: function(target, options) {
146                target = $(target);
147                if (target.hasClass(this.markerClassName)) {
148                        return;
149                }
150                var inline = !target[0].nodeName.toLowerCase().match(/input|textarea/);
151                var inst = {options: $.extend({}, this._defaults, options), _inline: inline,
152                        _mainDiv: (inline ? $('<div class="' + this._inlineClass + '"></div>') : plugin.mainDiv),
153                        ucase: false};
154                this._setInput(target, inst);
155                this._connectKeypad(target, inst);
156                if (inline) {
157                        target.append(inst._mainDiv).
158                                bind('click.' + this.propertyName, function() { inst._input.focus(); });
159                        this._updateKeypad(inst);
160                }
161                else if (target.is(':disabled')) {
162                        this._disablePlugin(target);
163                }
164        },
165
166        /* Determine the input field for the keypad.
167           @param  target  (jQuery) the target control
168           @param  inst    (object) the instance settings */
169        _setInput: function(target, inst) {
170                inst._input = $(!inst._inline ? target : inst.options.target ||
171                        '<input type="text" class="' + this._inlineEntryClass + '" disabled="disabled"/>');
172                if (inst._inline) {
173                        target.find('input').remove();
174                        if (!inst.options.target) {
175                                target.append(inst._input);
176                        }
177                }
178        },
179
180        /* Attach the keypad to a text field.
181           @param  target  (jQuery) the target text field
182           @param  inst    (object) the instance settings */
183        _connectKeypad: function(target, inst) {
184                target = $(target);
185                var appendText = inst.options.appendText;
186                if (appendText) {
187                        target[inst.options.isRTL ? 'before' : 'after'](
188                                '<span class="' + this._appendClass + '">' + appendText + '</span>');
189                }
190                if (!inst._inline) {
191                        if (inst.options.showOn == 'focus' || inst.options.showOn == 'both') {
192                                // pop-up keypad when in the marked field
193                                target.bind('focus.' + this.propertyName, this._showPlugin).
194                                        bind('keydown.' + this.propertyName, this._doKeyDown);
195                        }
196                        if (inst.options.showOn == 'button' || inst.options.showOn == 'both') {
197                                // pop-up keypad when button clicked
198                                var buttonStatus = inst.options.buttonStatus;
199                                var buttonImage = inst.options.buttonImage;
200                                var trigger = $(inst.options.buttonImageOnly ?
201                                        $('<img src="' + buttonImage + '" alt="' +
202                                        buttonStatus + '" title="' + buttonStatus + '"/>') :
203                                $('<button type="button" title="' + buttonStatus + '"></button>').
204                                        html(buttonImage == '' ? inst.options.buttonText :
205                                        $('<img src="' + buttonImage + '" alt="' +
206                                        buttonStatus + '" title="' + buttonStatus + '"/>')));
207                                target[inst.options.isRTL ? 'before' : 'after'](trigger);
208                                trigger.addClass(this._triggerClass).click(function() {
209                                        if (plugin._keypadShowing && plugin._lastField == target[0]) {
210                                                plugin._hidePlugin();
211                                        }
212                                        else {
213                                                plugin._showPlugin(target[0]);
214                                        }
215                                        return false;
216                                });
217                        }
218                }
219                inst.saveReadonly = target.attr('readonly');
220                target.addClass(this.markerClassName).
221                        data(this.propertyName, inst)
222                        [inst.options.keypadOnly ? 'attr' : 'removeAttr']('readonly', true).
223                        bind('setData.' + this.propertyName, function(event, key, value) {
224                                inst.options[key] = value;
225                        }).bind('getData.' + this.propertyName, function(event, key) {
226                                return inst.options[key];
227                        });
228        },
229
230        /* Retrieve or reconfigure the settings for a control.
231           @param  target   (element) the control to affect
232           @param  options  (object) the new options for this instance or
233                            (string) an individual property name
234           @param  value    (any) the individual property value (omit if options
235                            is an object or to retrieve the value of a setting)
236           @return  (any) if retrieving a value */
237        _optionPlugin: function(target, options, value) {
238                target = $(target);
239                var inst = target.data(this.propertyName);
240                if (!options || (typeof options == 'string' && value == null)) { // Get option
241                        var name = options;
242                        options = (inst || {}).options;
243                        return (options && name ? options[name] : options);
244                }
245
246                if (!target.hasClass(this.markerClassName)) {
247                        return;
248                }
249                options = options || {};
250                if (typeof options == 'string') {
251                        var name = options;
252                        options = {};
253                        options[name] = value;
254                }
255                if (this._curInst == inst) {
256                        this._hidePlugin();
257                }
258                $.extend(inst.options, options);
259                this._setInput(target, inst);
260                this._updateKeypad(inst);
261        },
262
263        /* Detach keypad from its control.
264           @param  target  (element) the target text field */
265        _destroyPlugin: function(target) {
266                target = $(target);
267                if (!target.hasClass(this.markerClassName)) {
268                        return;
269                }
270                var inst = target.data(this.propertyName);
271                if (this._curInst == inst) {
272                        this._hidePlugin();
273                }
274                target.siblings('.' + this._appendClass).remove().end().
275                        siblings('.' + this._triggerClass).remove().end().
276                        prev('.' + this._inlineEntryClass).remove();
277                target.removeClass(this.markerClassName).empty().
278                        unbind('.' + this.propertyName).
279                        removeData(this.propertyName)
280                        [inst.saveReadonly ? 'attr' : 'removeAttr']('readonly', true);
281                inst._input.removeData(this.propertyName);
282        },
283
284        /* Enable the keypad for a jQuery selection.
285           @param  target  (element) the target text field */
286        _enablePlugin: function(target) {
287                target = $(target);
288                if (!target.hasClass(this.markerClassName)) {
289                        return;
290                }
291                var nodeName = target[0].nodeName.toLowerCase();
292                if (nodeName.match(/input|textarea/)) {
293                        target[0].disabled = false;
294                        target.siblings('button.' + this._triggerClass).
295                                each(function() { this.disabled = false; }).end().
296                                siblings('img.' + this._triggerClass).
297                                css({opacity: '1.0', cursor: ''});
298                }
299                else if (nodeName.match(/div|span/)) {
300                        target.children('.' + this._disableClass).remove();
301                        var inst = target.data(this.propertyName);
302                        inst._mainDiv.find('button').removeAttr('disabled');
303                }
304                this._disabledFields = $.map(this._disabledFields,
305                        function(value) { return (value == target[0] ? null : value); }); // delete entry
306        },
307
308        /* Disable the keypad for a jQuery selection.
309           @param  target  (element) the target text field */
310        _disablePlugin: function(target) {
311                target = $(target);
312                if (!target.hasClass(this.markerClassName)) {
313                        return;
314                }
315                var nodeName = target[0].nodeName.toLowerCase();
316                if (nodeName.match(/input|textarea/)) {
317                        target[0].disabled = true;
318                        target.siblings('button.' + this._triggerClass).
319                                each(function() { this.disabled = true; }).end().
320                                siblings('img.' + this._triggerClass).
321                                css({opacity: '0.5', cursor: 'default'});
322                }
323                else if (nodeName.match(/div|span/)) {
324                        var inline = target.children('.' + this._inlineClass);
325                        var offset = inline.offset();
326                        var relOffset = {left: 0, top: 0};
327                        inline.parents().each(function() {
328                                if ($(this).css('position') == 'relative') {
329                                        relOffset = $(this).offset();
330                                        return false;
331                                }
332                        });
333                        target.prepend('<div class="' + this._disableClass + '" style="width: ' +
334                                inline.outerWidth() + 'px; height: ' + inline.outerHeight() +
335                                'px; left: ' + (offset.left - relOffset.left) +
336                                'px; top: ' + (offset.top - relOffset.top) + 'px;"></div>');
337                        var inst = target.data(this.propertyName);
338                        inst._mainDiv.find('button').attr('disabled', 'disabled');
339                }
340                this._disabledFields = $.map(this._disabledFields,
341                        function(value) { return (value == target[0] ? null : value); }); // delete entry
342                this._disabledFields[this._disabledFields.length] = target[0];
343        },
344
345        /* Is the text field disabled as a keypad?
346           @param  target  (element) the target text field
347           @return  (boolean) true if disabled, false if enabled */
348        _isDisabledPlugin: function(target) {
349                return (target && $.inArray(target, this._disabledFields) > -1);
350        },
351
352        /* Pop-up the keypad for a given text field.
353           @param  field  (element) the text field attached to the keypad or
354                          (event) if triggered by focus */
355        _showPlugin: function(field) {
356                field = field.target || field;
357                if (plugin._isDisabledPlugin(field) ||
358                                plugin._lastField == field) { // already here
359                        return;
360                }
361                var inst = $.data(field, plugin.propertyName);
362                plugin._hidePlugin(null, '');
363                plugin._lastField = field;
364                plugin._pos = plugin._findPos(field);
365                plugin._pos[1] += field.offsetHeight; // add the height
366                var isFixed = false;
367                $(field).parents().each(function() {
368                        isFixed |= $(this).css('position') == 'fixed';
369                        return !isFixed;
370                });
371                if (isFixed && $.browser.opera) { // correction for Opera when fixed and scrolled
372                        plugin._pos[0] -= document.documentElement.scrollLeft;
373                        plugin._pos[1] -= document.documentElement.scrollTop;
374                }
375                var offset = {left: plugin._pos[0], top: plugin._pos[1]};
376                plugin._pos = null;
377                // determine sizing offscreen
378                inst._mainDiv.css({position: 'absolute', display: 'block', top: '-1000px',
379                        width: ($.browser.opera ? '1000px' : 'auto')});
380                plugin._updateKeypad(inst);
381                // and adjust position before showing
382                offset = plugin._checkOffset(inst, offset, isFixed);
383                inst._mainDiv.css({position: (isFixed ? 'fixed' : 'absolute'), display: 'none',
384                        left: offset.left + 'px', top: offset.top + 'px'});
385                var duration = inst.options.duration;
386                duration = (duration == 'normal' && $.ui && $.ui.version >= '1.8' ? '_default' : duration);
387                var showAnim = inst.options.showAnim;
388                var postProcess = function() {
389                        plugin._keypadShowing = true;
390                        var borders = plugin._getBorders(inst._mainDiv);
391                        inst._mainDiv.find('iframe.' + plugin._coverClass). // IE6- only
392                                css({left: -borders[0], top: -borders[1],
393                                        width: inst._mainDiv.outerWidth(), height: inst._mainDiv.outerHeight()});
394                };
395                if ($.effects && ($.effects[showAnim] || ($.effects.effect && $.effects.effect[showAnim]))) {
396                        var data = inst._mainDiv.data(); // Update old effects data
397                        for (var key in data) {
398                                if (key.match(/^ec\.storage\./)) {
399                                        data[key] = inst._mainDiv.css(key.replace(/ec\.storage\./, ''));
400                                }
401                        }
402                        inst._mainDiv.data(data).show(showAnim,
403                                inst.options.showOptions, duration, postProcess);
404                }
405                else {
406                        inst._mainDiv[showAnim || 'show']((showAnim ? duration : ''), postProcess);
407                }
408                if (!showAnim) {
409                        postProcess();
410                }
411                if (inst._input[0].type != 'hidden') {
412                        inst._input[0].focus();
413                }
414                plugin._curInst = inst;
415        },
416
417        /* Generate the keypad content.
418           @param  inst  (object) the instance settings */
419        _updateKeypad: function(inst) {
420                var borders = this._getBorders(inst._mainDiv);
421                inst._mainDiv.empty().append(this._generateHTML(inst)).
422                        find('iframe.' + this._coverClass). // IE6- only
423                        css({left: -borders[0], top: -borders[1],
424                                width: inst._mainDiv.outerWidth(), height: inst._mainDiv.outerHeight()});
425                inst._mainDiv.removeClass().addClass(inst.options.keypadClass +
426                        (inst.options.useThemeRoller ? ' ui-widget ui-widget-content' : '') +
427                        (inst.options.isRTL ? ' ' + this._rtlClass : '') + ' ' +
428                        (inst._inline ? this._inlineClass : this._mainDivClass));
429                if ($.isFunction(inst.options.beforeShow)) {
430                        inst.options.beforeShow.apply((inst._input ? inst._input[0] : null),
431                                [inst._mainDiv, inst]);
432                }
433        },
434
435        /* Retrieve the size of left and top borders for an element.
436           @param  elem  (jQuery object) the element of interest
437           @return  (number[2]) the left and top borders */
438        _getBorders: function(elem) {
439                var convert = function(value) {
440                        var extra = ($.browser.msie ? 1 : 0);
441                        return {thin: 1 + extra, medium: 3 + extra, thick: 5 + extra}[value] || value;
442                };
443                return [parseFloat(convert(elem.css('border-left-width'))),
444                        parseFloat(convert(elem.css('border-top-width')))];
445        },
446
447        /* Check positioning to remain on screen.
448           @param  inst    (object) the instance settings
449           @param  offset  (object) the current offset
450           @param  isFixed  (boolean) true if the text field is fixed in position
451           @return  (object) the updated offset */
452        _checkOffset: function(inst, offset, isFixed) {
453                var pos = inst._input ? this._findPos(inst._input[0]) : null;
454                var browserWidth = window.innerWidth || document.documentElement.clientWidth;
455                var browserHeight = window.innerHeight || document.documentElement.clientHeight;
456                var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
457                var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
458                if (($.browser.msie && parseInt($.browser.version, 10) < 7) || $.browser.opera) {
459                        // recalculate width as otherwise set to 100%
460                        var width = 0;
461                        inst._mainDiv.find(':not(div,iframe)').each(function() {
462                                width = Math.max(width, this.offsetLeft + $(this).outerWidth() +
463                                        parseInt($(this).css('margin-right'), 10));
464                        });
465                        inst._mainDiv.css('width', width);
466                }
467                // reposition keypad panel horizontally if outside the browser window
468                if (inst.options.isRTL ||
469                                (offset.left + inst._mainDiv.outerWidth() - scrollX) > browserWidth) {
470                        offset.left = Math.max((isFixed ? 0 : scrollX),
471                                pos[0] + (inst._input ? inst._input.outerWidth() : 0) -
472                                (isFixed ? scrollX : 0) - inst._mainDiv.outerWidth() -
473                                (isFixed && $.browser.opera ? document.documentElement.scrollLeft : 0));
474                }
475                else {
476                        offset.left -= (isFixed ? scrollX : 0);
477                }
478                // reposition keypad panel vertically if outside the browser window
479                if ((offset.top + inst._mainDiv.outerHeight() - scrollY) > browserHeight) {
480                        offset.top = Math.max((isFixed ? 0 : scrollY),
481                                pos[1] - (isFixed ? scrollY : 0) - inst._mainDiv.outerHeight() -
482                                (isFixed && $.browser.opera ? document.documentElement.scrollTop : 0));
483                }
484                else {
485                        offset.top -= (isFixed ? scrollY : 0);
486                }
487                return offset;
488        },
489       
490        /* Find an object's position on the screen.
491           @param  obj  (element) the element to find the position for
492           @return  (int[2]) the element's position */
493        _findPos: function(obj) {
494        while (obj && (obj.type == 'hidden' || obj.nodeType != 1)) {
495            obj = obj.nextSibling;
496        }
497        var position = $(obj).offset();
498            return [position.left, position.top];
499        },
500
501        /* Hide the keypad from view.
502           @param  field     (element) the text field attached to the keypad
503           @param  duration  (string) the duration over which to close the keypad */
504        _hidePlugin: function(field, duration) {
505                var inst = this._curInst;
506                if (!inst || (field && inst != $.data(field, this.propertyName))) {
507                        return;
508                }
509                if (this._keypadShowing) {
510                        duration = (duration != null ? duration : inst.options.duration);
511                        duration = (duration == 'normal' && $.ui && $.ui.version >= '1.8' ? '_default' : duration);
512                        var showAnim = inst.options.showAnim;
513                        if ($.effects && ($.effects[showAnim] || ($.effects.effect && $.effects.effect[showAnim]))) {
514                                inst._mainDiv.hide(showAnim, inst.options.showOptions, duration);
515                        }
516                        else {
517                                inst._mainDiv[(showAnim == 'slideDown' ? 'slideUp' :
518                                        (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))](showAnim ? duration : '');
519                        }
520                }
521                if ($.isFunction(inst.options.onClose)) {
522                        inst.options.onClose.apply((inst._input ? inst._input[0] : null),  // trigger custom callback
523                                [inst._input.val(), inst]);
524                }
525                if (this._keypadShowing) {
526                        this._keypadShowing = false;
527                        this._lastField = null;
528                }
529                if (inst._inline) {
530                        inst._input.val('');
531                }
532                this._curInst = null;
533        },
534
535        /* Handle keystrokes.
536           @param  e  (event) the key event */
537        _doKeyDown: function(e) {
538                if (e.keyCode == 9) { // Tab out
539                        plugin.mainDiv.stop(true, true);
540                        plugin._hidePlugin();
541                }
542        },
543
544        /* Close keypad if clicked elsewhere.
545           @param  event  (event) the mouseclick details */
546        _checkExternalClick: function(event) {
547                if (!plugin._curInst) {
548                        return;
549                }
550                var target = $(event.target);
551                if (!target.parents().andSelf().hasClass(plugin._mainDivClass) &&
552                                !target.hasClass(plugin.markerClassName) &&
553                                !target.parents().andSelf().hasClass(plugin._triggerClass) &&
554                                plugin._keypadShowing) {
555                        plugin._hidePlugin();
556                }
557        },
558
559        /* Toggle between upper and lower case.
560           @param  inst  (object) the instance settings */
561        _shiftKeypad: function(inst) {
562                inst.ucase = !inst.ucase;
563                this._updateKeypad(inst);
564                inst._input.focus(); // for further typing
565        },
566
567        /* Erase the text field.
568           @param  inst  (object) the instance settings */
569        _clearValue: function(inst) {
570                this._setValue(inst, '', 0);
571                this._notifyKeypress(inst, plugin.DEL);
572        },
573
574        /* Erase the last character.
575           @param  inst  (object) the instance settings */
576        _backValue: function(inst) {
577                var field = inst._input[0];
578                var value = inst._input.val();
579                var range = [value.length, value.length];
580                if (field.setSelectionRange) { // Mozilla
581                        range = (inst._input.attr('readonly') || inst._input.attr('disabled') ?
582                                range : [field.selectionStart, field.selectionEnd]);
583                }
584                else if (field.createTextRange) { // IE
585                        range = (inst._input.attr('readonly') || inst._input.attr('disabled') ?
586                                range : this._getIERange(field));
587                }
588                this._setValue(inst, (value.length == 0 ? '' :
589                        value.substr(0, range[0] - 1) + value.substr(range[1])), range[0] - 1);
590                this._notifyKeypress(inst, plugin.BS);
591        },
592
593        /* Update the text field with the selected value.
594           @param  inst   (object) the instance settings
595           @param  value  (string) the new character to add */
596        _selectValue: function(inst, value) {
597                this.insertValue(inst._input[0], value);
598                this._setValue(inst, inst._input.val());
599                this._notifyKeypress(inst, value);
600        },
601
602        /* Update the text field with the selected value.
603           @param  input  (element) the input field or
604                          (jQuery) jQuery collection
605           @param  value  (string) the new character to add */
606        insertValue: function(input, value) {
607                input = (input.jquery ? input : $(input));
608                var field = input[0];
609                var newValue = input.val();
610                var range = [newValue.length, newValue.length];
611                if (field.setSelectionRange) { // Mozilla
612                        range = (input.attr('readonly') || input.attr('disabled') ?
613                                range : [field.selectionStart, field.selectionEnd]);
614                }
615                else if (field.createTextRange) { // IE
616                        range = (input.attr('readonly') || input.attr('disabled') ?
617                                range : this._getIERange(field));
618                }
619                input.val(newValue.substr(0, range[0]) + value + newValue.substr(range[1]));
620                pos = range[0] + value.length;
621                if (input.is(':visible')) {
622                        input.focus(); // for further typing
623                }
624                if (field.setSelectionRange) { // Mozilla
625                        if (input.is(':visible')) {
626                                field.setSelectionRange(pos, pos);
627                        }
628                }
629                else if (field.createTextRange) { // IE
630                        range = field.createTextRange();
631                        range.move('character', pos);
632                        range.select();
633                }
634        },
635
636        /* Get the coordinates for the selected area in the text field in IE.
637           @param  field  (element) the target text field
638           @return  (int[2]) the start and end positions of the selection */
639        _getIERange: function(field) {
640                field.focus();
641                var selectionRange = document.selection.createRange().duplicate();
642                // Use two ranges: before and selection
643                var beforeRange = this._getIETextRange(field);
644                beforeRange.setEndPoint('EndToStart', selectionRange);
645                // Check each range for trimmed newlines by shrinking the range by one
646                // character and seeing if the text property has changed. If it has not
647                // changed then we know that IE has trimmed a \r\n from the end.
648                var checkCRLF = function(range) {
649                        var origText = range.text;
650                        var text = origText;
651                        var finished = false;
652                        while (true) {
653                                if (range.compareEndPoints('StartToEnd', range) == 0) {
654                                        break;
655                                }
656                                else {
657                                        range.moveEnd('character', -1);
658                                        if (range.text == origText) {
659                                                text += '\r\n';
660                                        }
661                                        else {
662                                                break;
663                                        }
664                                }
665                        }
666                        return text;
667                };
668                var beforeText = checkCRLF(beforeRange);
669                var selectionText = checkCRLF(selectionRange);
670                return [beforeText.length, beforeText.length + selectionText.length];
671        },
672
673        /* Create an IE text range for the text field.
674           @param  field  (element) the target text field
675           @return  (object) the corresponding text range */
676        _getIETextRange: function(field) {
677                var isInput = (field.nodeName.toLowerCase() == 'input');
678                var range = (isInput ? field.createTextRange() : document.body.createTextRange());
679                if (!isInput) {
680                        range.moveToElementText(field); // Selects all the text for a textarea
681                }
682                return range;
683        },
684
685        /* Set the text field to the selected value,
686           and trigger any on change event.
687           @param  inst   (object) the instance settings
688           @param  value  (string) the new value for the text field */
689        _setValue: function(inst, value) {
690                var maxlen = inst._input.attr('maxlength');
691                if (maxlen > -1) {
692                        value = value.substr(0, maxlen);
693                }
694                inst._input.val(value);
695                if (!$.isFunction(inst.options.onKeypress)) {
696                        inst._input.trigger('change'); // fire the change event
697                }
698        },
699
700        _notifyKeypress: function(inst, key) {
701                if ($.isFunction(inst.options.onKeypress)) { // trigger custom callback
702                        inst.options.onKeypress.apply((inst._input ? inst._input[0] : null),
703                                [key, inst._input.val(), inst]);
704                }
705        },
706
707        /* Generate the HTML for the current state of the keypad.
708           @param  inst  (object) the instance settings
709           @return  (jQuery) the HTML for this keypad */
710        _generateHTML: function(inst) {
711                var html = (!inst.options.prompt ? '' : '<div class="' + this._promptClass +
712                        (inst.options.useThemeRoller ? ' ui-widget-header ui-corner-all' : '') + '">' +
713                        inst.options.prompt + '</div>');
714                var layout = this._randomiseLayout(inst);
715                for (var i = 0; i < layout.length; i++) {
716                        html += '<div class="' + this._rowClass + '">';
717                        var keys = layout[i].split(inst.options.separator);
718                        for (var j = 0; j < keys.length; j++) {
719                                if (inst.ucase) {
720                                        keys[j] = inst.options.toUpper(keys[j]);
721                                }
722                                var keyDef = this._specialKeys[keys[j].charCodeAt(0)];
723                                if (keyDef) {
724                                        html += (keyDef.action ? '<button type="button" class="' + this._specialClass +
725                                                ' ' + this._namePrefixClass + keyDef.name +
726                                                (inst.options.useThemeRoller ? ' ui-corner-all ui-state-default' +
727                                                (keyDef.noHighlight ? '' : ' ui-state-highlight') : '') +
728                                                '" title="' + inst.options[keyDef.name + 'Status'] + '">' +
729                                                (inst.options[keyDef.name + 'Text'] || '&nbsp;') + '</button>' :
730                                                '<div class="' + this._namePrefixClass + keyDef.name + '"></div>');
731                                }
732                                else {
733                                        html += '<button type="button" class="' + this._keyClass +
734                                                (inst.options.useThemeRoller ? ' ui-corner-all ui-state-default' : '') + '">' +
735                                                (keys[j] == ' ' ? '&nbsp;' : keys[j]) + '</button>';
736                                }
737                        }
738                        html += '</div>';
739                }
740                html += '<div style="clear: both;"></div>' +
741                        (!inst._inline && $.browser.msie && parseInt($.browser.version, 10) < 7 ?
742                        '<iframe src="javascript:false;" class="' + plugin._coverClass + '"></iframe>' : '');
743                html = $(html);
744                var thisInst = inst;
745                var activeClasses = this._keyDownClass + (inst.options.useThemeRoller ? ' ui-state-active' : '');
746                html.find('button').mousedown(function() { $(this).addClass(activeClasses); }).
747                        mouseup(function() { $(this).removeClass(activeClasses); }).
748                        mouseout(function() { $(this).removeClass(activeClasses); }).
749                        filter('.' + this._keyClass).click(function() { plugin._selectValue(thisInst, $(this).text()); });
750                $.each(this._specialKeys, function(i, keyDef) {
751                        html.find('.' + plugin._namePrefixClass + keyDef.name).click(function() {
752                                keyDef.action.apply(thisInst._input, [thisInst]);
753                        });
754                });
755                return html;
756        },
757
758        /* Check whether characters should be randomised,
759           and, if so, produce the randomised layout.
760           @param  inst  (object) the instance settings
761           @return  (string[]) the layout with any requested randomisations applied */
762        _randomiseLayout: function(inst) {
763                if (!inst.options.randomiseNumeric && !inst.options.randomiseAlphabetic &&
764                                !inst.options.randomiseOther && !inst.options.randomiseAll) {
765                        return inst.options.layout;
766                }
767                var numerics = [];
768                var alphas = [];
769                var others = [];
770                var newLayout = [];
771                // Find characters of different types
772                for (var i = 0; i < inst.options.layout.length; i++) {
773                        newLayout[i] = '';
774                        var keys = inst.options.layout[i].split(inst.options.separator);
775                        for (var j = 0; j < keys.length; j++) {
776                                if (this._isControl(keys[j])) {
777                                        continue;
778                                }
779                                if (inst.options.randomiseAll) {
780                                        others.push(keys[j]);
781                                }
782                                else if (inst.options.isNumeric(keys[j])) {
783                                        numerics.push(keys[j]);
784                                }
785                                else if (inst.options.isAlphabetic(keys[j])) {
786                                        alphas.push(keys[j]);
787                                }
788                                else {
789                                        others.push(keys[j]);
790                                }
791                        }
792                }
793                // Shuffle them
794                if (inst.options.randomiseNumeric) {
795                        this._shuffle(numerics);
796                }
797                if (inst.options.randomiseAlphabetic) {
798                        this._shuffle(alphas);
799                }
800                if (inst.options.randomiseOther || inst.options.randomiseAll) {
801                        this._shuffle(others);
802                }
803                var n = 0;
804                var a = 0;
805                var o = 0;
806                // And replace them in the layout
807                for (var i = 0; i < inst.options.layout.length; i++) {
808                        var keys = inst.options.layout[i].split(inst.options.separator);
809                        for (var j = 0; j < keys.length; j++) {
810                                newLayout[i] += (this._isControl(keys[j]) ? keys[j] :
811                                        (inst.options.randomiseAll ? others[o++] :
812                                        (inst.options.isNumeric(keys[j]) ? numerics[n++] :
813                                        (inst.options.isAlphabetic(keys[j]) ? alphas[a++] :
814                                        others[o++])))) + inst.options.separator;
815                        }
816                }
817                return newLayout;
818        },
819
820        /* Is a given character a control character?
821           @param  ch  (char) the character to test
822           @return  (boolean) true if a control character, false if not */
823        _isControl: function(ch) {
824                return ch < ' ';
825        },
826
827        /* Is a given character alphabetic?
828           @param  ch  (char) the character to test
829           @return  (boolean) true if alphabetic, false if not */
830        isAlphabetic: function(ch) {
831                return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
832        },
833
834        /* Is a given character numeric?
835           @param  ch  (char) the character to test
836           @return  (boolean) true if numeric, false if not */
837        isNumeric: function(ch) {
838                return (ch >= '0' && ch <= '9');
839        },
840
841        /* Convert a character to upper case.
842           @param  ch  (char) the character to convert
843           @return  (char) its uppercase version */
844        toUpper: function(ch) {
845                return ch.toUpperCase();
846        },
847        /* Randomise the contents of an array.
848           @param  values  (string[]) the array to rearrange */
849        _shuffle: function(values) {
850                for (var i = values.length - 1; i > 0; i--) {
851                        var j = Math.floor(Math.random() * values.length);
852                        var ch = values[i];
853                        values[i] = values[j];
854                        values[j] = ch;
855                }
856        }
857});
858
859// The list of commands that return values and don't permit chaining
860var getters = ['isDisabled'];
861
862/* Determine whether a command is a getter and doesn't permit chaining.
863   @param  command    (string, optional) the command to run
864   @param  otherArgs  ([], optional) any other arguments for the command
865   @return  true if the command is a getter, false if not */
866function isNotChained(command, otherArgs) {
867        if (command == 'option' && (otherArgs.length == 0 ||
868                        (otherArgs.length == 1 && typeof otherArgs[0] == 'string'))) {
869                return true;
870        }
871        return $.inArray(command, getters) > -1;
872}
873
874/* Invoke the keypad functionality.
875   @param  options  (object) the new settings to use for these instances (optional) or
876                    (string) the command to run (optional)
877   @return  (jQuery) for chaining further calls or
878            (any) getter value */
879$.fn.keypad = function(options) {
880        var otherArgs = Array.prototype.slice.call(arguments, 1);
881        if (isNotChained(options, otherArgs)) {
882                return plugin['_' + options + 'Plugin'].apply(plugin, [this[0]].concat(otherArgs));
883        }
884        return this.each(function() {
885                if (typeof options == 'string') {
886                        if (!plugin['_' + options + 'Plugin']) {
887                                throw 'Unknown command: ' + options;
888                        }
889                        plugin['_' + options + 'Plugin'].apply(plugin, [this].concat(otherArgs));
890                }
891                else {
892                        plugin._attachPlugin(this, options || {});
893                }
894        });
895};
896
897/* Initialise the keypad functionality. */
898var plugin = $.keypad = new Keypad(); // Singleton instance
899
900// Add the keypad division and external click check
901$(function() {
902        $(document.body).append(plugin.mainDiv).
903                mousedown(plugin._checkExternalClick);
904});
905
906})(jQuery);
Note: See TracBrowser for help on using the repository browser.