Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('autocomplete-base', function (Y, NAME) {
2
 
3
/**
4
Provides automatic input completion or suggestions for text input fields and
5
textareas.
6
 
7
@module autocomplete
8
@main autocomplete
9
@since 3.3.0
10
**/
11
 
12
/**
13
`Y.Base` extension that provides core autocomplete logic (but no UI
14
implementation) for a text input field or textarea. Must be mixed into a
15
`Y.Base`-derived class to be useful.
16
 
17
@module autocomplete
18
@submodule autocomplete-base
19
**/
20
 
21
/**
22
Extension that provides core autocomplete logic (but no UI implementation) for a
23
text input field or textarea.
24
 
25
The `AutoCompleteBase` class provides events and attributes that abstract away
26
core autocomplete logic and configuration, but does not provide a widget
27
implementation or suggestion UI. For a prepackaged autocomplete widget, see
28
`AutoCompleteList`.
29
 
30
This extension cannot be instantiated directly, since it doesn't provide an
31
actual implementation. It's intended to be mixed into a `Y.Base`-based class or
32
widget.
33
 
34
`Y.Widget`-based example:
35
 
36
    YUI().use('autocomplete-base', 'widget', function (Y) {
37
        var MyAC = Y.Base.create('myAC', Y.Widget, [Y.AutoCompleteBase], {
38
            // Custom prototype methods and properties.
39
        }, {
40
            // Custom static methods and properties.
41
        });
42
 
43
        // Custom implementation code.
44
    });
45
 
46
`Y.Base`-based example:
47
 
48
    YUI().use('autocomplete-base', function (Y) {
49
        var MyAC = Y.Base.create('myAC', Y.Base, [Y.AutoCompleteBase], {
50
            initializer: function () {
51
                this._bindUIACBase();
52
                this._syncUIACBase();
53
            },
54
 
55
            // Custom prototype methods and properties.
56
        }, {
57
            // Custom static methods and properties.
58
        });
59
 
60
        // Custom implementation code.
61
    });
62
 
63
@class AutoCompleteBase
64
**/
65
 
66
var Escape  = Y.Escape,
67
    Lang    = Y.Lang,
68
    YArray  = Y.Array,
69
    YObject = Y.Object,
70
 
71
    isFunction = Lang.isFunction,
72
    isString   = Lang.isString,
73
    trim       = Lang.trim,
74
 
75
    INVALID_VALUE = Y.Attribute.INVALID_VALUE,
76
 
77
    _FUNCTION_VALIDATOR = '_functionValidator',
78
    _SOURCE_SUCCESS     = '_sourceSuccess',
79
 
80
    ALLOW_BROWSER_AC    = 'allowBrowserAutocomplete',
81
    INPUT_NODE          = 'inputNode',
82
    QUERY               = 'query',
83
    QUERY_DELIMITER     = 'queryDelimiter',
84
    REQUEST_TEMPLATE    = 'requestTemplate',
85
    RESULTS             = 'results',
86
    RESULT_LIST_LOCATOR = 'resultListLocator',
87
    VALUE               = 'value',
88
    VALUE_CHANGE        = 'valueChange',
89
 
90
    EVT_CLEAR   = 'clear',
91
    EVT_QUERY   = QUERY,
92
    EVT_RESULTS = RESULTS;
93
 
94
function AutoCompleteBase() {}
95
 
96
AutoCompleteBase.prototype = {
97
    // -- Lifecycle Methods ----------------------------------------------------
98
    initializer: function () {
99
        // AOP bindings.
100
        Y.before(this._bindUIACBase, this, 'bindUI');
101
        Y.before(this._syncUIACBase, this, 'syncUI');
102
 
103
        // -- Public Events ----------------------------------------------------
104
 
105
        /**
106
        Fires after the query has been completely cleared or no longer meets the
107
        minimum query length requirement.
108
 
109
        @event clear
110
        @param {String} prevVal Value of the query before it was cleared.
111
        @param {String} src Source of the event.
112
        @preventable _defClearFn
113
        **/
114
        this.publish(EVT_CLEAR, {
115
            defaultFn: this._defClearFn
116
        });
117
 
118
        /**
119
        Fires when the contents of the input field have changed and the input
120
        value meets the criteria necessary to generate an autocomplete query.
121
 
122
        @event query
123
        @param {String} inputValue Full contents of the text input field or
124
            textarea that generated the query.
125
        @param {String} query AutoComplete query. This is the string that will
126
            be used to request completion results. It may or may not be the same
127
            as `inputValue`.
128
        @param {String} src Source of the event.
129
        @preventable _defQueryFn
130
        **/
131
        this.publish(EVT_QUERY, {
132
            defaultFn: this._defQueryFn
133
        });
134
 
135
        /**
136
        Fires after query results are received from the source. If no source has
137
        been set, this event will not fire.
138
 
139
        @event results
140
        @param {Array|Object} data Raw, unfiltered result data (if available).
141
        @param {String} query Query that generated these results.
142
        @param {Object[]} results Array of filtered, formatted, and highlighted
143
            results. Each item in the array is an object with the following
144
            properties:
145
 
146
            @param {Node|HTMLElement|String} results.display Formatted result
147
                HTML suitable for display to the user. If no custom formatter is
148
                set, this will be an HTML-escaped version of the string in the
149
                `text` property.
150
            @param {String} [results.highlighted] Highlighted (but not
151
                formatted) result text. This property will only be set if a
152
                highlighter is in use.
153
            @param {Any} results.raw Raw, unformatted result in whatever form it
154
                was provided by the source.
155
            @param {String} results.text Plain text version of the result,
156
                suitable for being inserted into the value of a text input field
157
                or textarea when the result is selected by a user. This value is
158
                not HTML-escaped and should not be inserted into the page using
159
                `innerHTML` or `Node#setContent()`.
160
 
161
        @preventable _defResultsFn
162
        **/
163
        this.publish(EVT_RESULTS, {
164
            defaultFn: this._defResultsFn
165
        });
166
    },
167
 
168
    destructor: function () {
169
        this._acBaseEvents && this._acBaseEvents.detach();
170
 
171
        delete this._acBaseEvents;
172
        delete this._cache;
173
        delete this._inputNode;
174
        delete this._rawSource;
175
    },
176
 
177
    // -- Public Prototype Methods ---------------------------------------------
178
 
179
    /**
180
    Clears the result cache.
181
 
182
    @method clearCache
183
    @chainable
184
    @since 3.5.0
185
    **/
186
    clearCache: function () {
187
        this._cache && (this._cache = {});
188
        return this;
189
    },
190
 
191
    /**
192
    Sends a request to the configured source. If no source is configured, this
193
    method won't do anything.
194
 
195
    Usually there's no reason to call this method manually; it will be called
196
    automatically when user input causes a `query` event to be fired. The only
197
    time you'll need to call this method manually is if you want to force a
198
    request to be sent when no user input has occurred.
199
 
200
    @method sendRequest
201
    @param {String} [query] Query to send. If specified, the `query` attribute
202
        will be set to this query. If not specified, the current value of the
203
        `query` attribute will be used.
204
    @param {Function} [requestTemplate] Request template function. If not
205
        specified, the current value of the `requestTemplate` attribute will be
206
        used.
207
    @chainable
208
    **/
209
    sendRequest: function (query, requestTemplate) {
210
        var request,
211
            source = this.get('source');
212
 
213
        if (query || query === '') {
214
            this._set(QUERY, query);
215
        } else {
216
            query = this.get(QUERY) || '';
217
        }
218
 
219
        if (source) {
220
            if (!requestTemplate) {
221
                requestTemplate = this.get(REQUEST_TEMPLATE);
222
            }
223
 
224
            request = requestTemplate ?
225
                requestTemplate.call(this, query) : query;
226
 
227
 
228
            source.sendRequest({
229
                query  : query,
230
                request: request,
231
 
232
                callback: {
233
                    success: Y.bind(this._onResponse, this, query)
234
                }
235
            });
236
        }
237
 
238
        return this;
239
    },
240
 
241
    // -- Protected Lifecycle Methods ------------------------------------------
242
 
243
    /**
244
    Attaches event listeners and behaviors.
245
 
246
    @method _bindUIACBase
247
    @protected
248
    **/
249
    _bindUIACBase: function () {
250
        var inputNode  = this.get(INPUT_NODE),
251
            tokenInput = inputNode && inputNode.tokenInput;
252
 
253
        // If the inputNode has a node-tokeninput plugin attached, bind to the
254
        // plugin's inputNode instead.
255
        if (tokenInput) {
256
            inputNode = tokenInput.get(INPUT_NODE);
257
            this._set('tokenInput', tokenInput);
258
        }
259
 
260
        if (!inputNode) {
261
            Y.error('No inputNode specified.');
262
            return;
263
        }
264
 
265
        this._inputNode = inputNode;
266
 
267
        this._acBaseEvents = new Y.EventHandle([
268
            // This is the valueChange event on the inputNode, provided by the
269
            // event-valuechange module, not our own valueChange.
270
            inputNode.on(VALUE_CHANGE, this._onInputValueChange, this),
271
            inputNode.on('blur', this._onInputBlur, this),
272
 
273
            this.after(ALLOW_BROWSER_AC + 'Change', this._syncBrowserAutocomplete),
274
            this.after('sourceTypeChange', this._afterSourceTypeChange),
275
            this.after(VALUE_CHANGE, this._afterValueChange)
276
        ]);
277
    },
278
 
279
    /**
280
    Synchronizes the UI state of the `inputNode`.
281
 
282
    @method _syncUIACBase
283
    @protected
284
    **/
285
    _syncUIACBase: function () {
286
        this._syncBrowserAutocomplete();
287
        this.set(VALUE, this.get(INPUT_NODE).get(VALUE));
288
    },
289
 
290
    // -- Protected Prototype Methods ------------------------------------------
291
 
292
    /**
293
    Creates a DataSource-like object that simply returns the specified array as
294
    a response. See the `source` attribute for more details.
295
 
296
    @method _createArraySource
297
    @param {Array} source
298
    @return {Object} DataSource-like object.
299
    @protected
300
    **/
301
    _createArraySource: function (source) {
302
        var that = this;
303
 
304
        return {
305
            type: 'array',
306
            sendRequest: function (request) {
307
                that[_SOURCE_SUCCESS](source.concat(), request);
308
            }
309
        };
310
    },
311
 
312
    /**
313
    Creates a DataSource-like object that passes the query to a custom-defined
314
    function, which is expected to call the provided callback with an array of
315
    results. See the `source` attribute for more details.
316
 
317
    @method _createFunctionSource
318
    @param {Function} source Function that accepts a query and a callback as
319
      parameters, and calls the callback with an array of results.
320
    @return {Object} DataSource-like object.
321
    @protected
322
    **/
323
    _createFunctionSource: function (source) {
324
        var that = this;
325
 
326
        return {
327
            type: 'function',
328
            sendRequest: function (request) {
329
                var value;
330
 
331
                function afterResults(results) {
332
                    that[_SOURCE_SUCCESS](results || [], request);
333
                }
334
 
335
                // Allow both synchronous and asynchronous functions. If we get
336
                // a truthy return value, assume the function is synchronous.
337
                if ((value = source(request.query, afterResults))) {
338
                    afterResults(value);
339
                }
340
            }
341
        };
342
    },
343
 
344
    /**
345
    Creates a DataSource-like object that looks up queries as properties on the
346
    specified object, and returns the found value (if any) as a response. See
347
    the `source` attribute for more details.
348
 
349
    @method _createObjectSource
350
    @param {Object} source
351
    @return {Object} DataSource-like object.
352
    @protected
353
    **/
354
    _createObjectSource: function (source) {
355
        var that = this;
356
 
357
        return {
358
            type: 'object',
359
            sendRequest: function (request) {
360
                var query = request.query;
361
 
362
                that[_SOURCE_SUCCESS](
363
                    YObject.owns(source, query) ? source[query] : [],
364
                    request
365
                );
366
            }
367
        };
368
    },
369
 
370
    /**
371
    Returns `true` if _value_ is either a function or `null`.
372
 
373
    @method _functionValidator
374
    @param {Function|null} value Value to validate.
375
    @protected
376
    **/
377
    _functionValidator: function (value) {
378
        return value === null || isFunction(value);
379
    },
380
 
381
    /**
382
    Faster and safer alternative to `Y.Object.getValue()`. Doesn't bother
383
    casting the path to an array (since we already know it's an array) and
384
    doesn't throw an error if a value in the middle of the object hierarchy is
385
    neither `undefined` nor an object.
386
 
387
    @method _getObjectValue
388
    @param {Object} obj
389
    @param {Array} path
390
    @return {Any} Located value, or `undefined` if the value was
391
        not found at the specified path.
392
    @protected
393
    **/
394
    _getObjectValue: function (obj, path) {
395
        if (!obj) {
396
            return;
397
        }
398
 
399
        for (var i = 0, len = path.length; obj && i < len; i++) {
400
            obj = obj[path[i]];
401
        }
402
 
403
        return obj;
404
    },
405
 
406
    /**
407
    Parses result responses, performs filtering and highlighting, and fires the
408
    `results` event.
409
 
410
    @method _parseResponse
411
    @param {String} query Query that generated these results.
412
    @param {Object} response Response containing results.
413
    @param {Object} data Raw response data.
414
    @protected
415
    **/
416
    _parseResponse: function (query, response, data) {
417
        var facade = {
418
                data   : data,
419
                query  : query,
420
                results: []
421
            },
422
 
423
            listLocator = this.get(RESULT_LIST_LOCATOR),
424
            results     = [],
425
            unfiltered  = response && response.results,
426
 
427
            filters,
428
            formatted,
429
            formatter,
430
            highlighted,
431
            highlighter,
432
            i,
433
            len,
434
            maxResults,
435
            result,
436
            text,
437
            textLocator;
438
 
439
        if (unfiltered && listLocator) {
440
            unfiltered = listLocator.call(this, unfiltered);
441
        }
442
 
443
        if (unfiltered && unfiltered.length) {
444
            filters     = this.get('resultFilters');
445
            textLocator = this.get('resultTextLocator');
446
 
447
            // Create a lightweight result object for each result to make them
448
            // easier to work with. The various properties on the object
449
            // represent different formats of the result, and will be populated
450
            // as we go.
451
            for (i = 0, len = unfiltered.length; i < len; ++i) {
452
                result = unfiltered[i];
453
 
454
                text = textLocator ?
455
                        textLocator.call(this, result) :
456
                        result.toString();
457
 
458
                results.push({
459
                    display: Escape.html(text),
460
                    raw    : result,
461
                    text   : text
462
                });
463
            }
464
 
465
            // Run the results through all configured result filters. Each
466
            // filter returns an array of (potentially fewer) result objects,
467
            // which is then passed to the next filter, and so on.
468
            for (i = 0, len = filters.length; i < len; ++i) {
469
                results = filters[i].call(this, query, results.concat());
470
 
471
                if (!results) {
472
                    return;
473
                }
474
 
475
                if (!results.length) {
476
                    break;
477
                }
478
            }
479
 
480
            if (results.length) {
481
                formatter   = this.get('resultFormatter');
482
                highlighter = this.get('resultHighlighter');
483
                maxResults  = this.get('maxResults');
484
 
485
                // If maxResults is set and greater than 0, limit the number of
486
                // results.
487
                if (maxResults && maxResults > 0 &&
488
                        results.length > maxResults) {
489
                    results.length = maxResults;
490
                }
491
 
492
                // Run the results through the configured highlighter (if any).
493
                // The highlighter returns an array of highlighted strings (not
494
                // an array of result objects), and these strings are then added
495
                // to each result object.
496
                if (highlighter) {
497
                    highlighted = highlighter.call(this, query,
498
                            results.concat());
499
 
500
                    if (!highlighted) {
501
                        return;
502
                    }
503
 
504
                    for (i = 0, len = highlighted.length; i < len; ++i) {
505
                        result = results[i];
506
                        result.highlighted = highlighted[i];
507
                        result.display     = result.highlighted;
508
                    }
509
                }
510
 
511
                // Run the results through the configured formatter (if any) to
512
                // produce the final formatted results. The formatter returns an
513
                // array of strings or Node instances (not an array of result
514
                // objects), and these strings/Nodes are then added to each
515
                // result object.
516
                if (formatter) {
517
                    formatted = formatter.call(this, query, results.concat());
518
 
519
                    if (!formatted) {
520
                        return;
521
                    }
522
 
523
                    for (i = 0, len = formatted.length; i < len; ++i) {
524
                        results[i].display = formatted[i];
525
                    }
526
                }
527
            }
528
        }
529
 
530
        facade.results = results;
531
        this.fire(EVT_RESULTS, facade);
532
    },
533
 
534
    /**
535
    Returns the query portion of the specified input value, or `null` if there
536
    is no suitable query within the input value.
537
 
538
    If a query delimiter is defined, the query will be the last delimited part
539
    of of the string.
540
 
541
    @method _parseValue
542
    @param {String} value Input value from which to extract the query.
543
    @return {String|null} query
544
    @protected
545
    **/
546
    _parseValue: function (value) {
547
        var delim = this.get(QUERY_DELIMITER);
548
 
549
        if (delim) {
550
            value = value.split(delim);
551
            value = value[value.length - 1];
552
        }
553
 
554
        return Lang.trimLeft(value);
555
    },
556
 
557
    /**
558
    Setter for the `enableCache` attribute.
559
 
560
    @method _setEnableCache
561
    @param {Boolean} value
562
    @protected
563
    @since 3.5.0
564
    **/
565
    _setEnableCache: function (value) {
566
        // When `this._cache` is an object, result sources will store cached
567
        // results in it. When it's falsy, they won't. This way result sources
568
        // don't need to get the value of the `enableCache` attribute on every
569
        // request, which would be sloooow.
570
        this._cache = value ? {} : null;
571
    },
572
 
573
    /**
574
    Setter for locator attributes.
575
 
576
    @method _setLocator
577
    @param {Function|String|null} locator
578
    @return {Function|null}
579
    @protected
580
    **/
581
    _setLocator: function (locator) {
582
        if (this[_FUNCTION_VALIDATOR](locator)) {
583
            return locator;
584
        }
585
 
586
        var that = this;
587
 
588
        locator = locator.toString().split('.');
589
 
590
        return function (result) {
591
            return result && that._getObjectValue(result, locator);
592
        };
593
    },
594
 
595
    /**
596
    Setter for the `requestTemplate` attribute.
597
 
598
    @method _setRequestTemplate
599
    @param {Function|String|null} template
600
    @return {Function|null}
601
    @protected
602
    **/
603
    _setRequestTemplate: function (template) {
604
        if (this[_FUNCTION_VALIDATOR](template)) {
605
            return template;
606
        }
607
 
608
        template = template.toString();
609
 
610
        return function (query) {
611
            return Lang.sub(template, {query: encodeURIComponent(query)});
612
        };
613
    },
614
 
615
    /**
616
    Setter for the `resultFilters` attribute.
617
 
618
    @method _setResultFilters
619
    @param {Array|Function|String|null} filters `null`, a filter
620
        function, an array of filter functions, or a string or array of strings
621
        representing the names of methods on `Y.AutoCompleteFilters`.
622
    @return {Function[]} Array of filter functions (empty if <i>filters</i> is
623
        `null`).
624
    @protected
625
    **/
626
    _setResultFilters: function (filters) {
627
        var acFilters, getFilterFunction;
628
 
629
        if (filters === null) {
630
            return [];
631
        }
632
 
633
        acFilters = Y.AutoCompleteFilters;
634
 
635
        getFilterFunction = function (filter) {
636
            if (isFunction(filter)) {
637
                return filter;
638
            }
639
 
640
            if (isString(filter) && acFilters &&
641
                    isFunction(acFilters[filter])) {
642
                return acFilters[filter];
643
            }
644
 
645
            return false;
646
        };
647
 
648
        if (Lang.isArray(filters)) {
649
            filters = YArray.map(filters, getFilterFunction);
650
            return YArray.every(filters, function (f) { return !!f; }) ?
651
                    filters : INVALID_VALUE;
652
        } else {
653
            filters = getFilterFunction(filters);
654
            return filters ? [filters] : INVALID_VALUE;
655
        }
656
    },
657
 
658
    /**
659
    Setter for the `resultHighlighter` attribute.
660
 
661
    @method _setResultHighlighter
662
    @param {Function|String|null} highlighter `null`, a highlighter function, or
663
        a string representing the name of a method on
664
        `Y.AutoCompleteHighlighters`.
665
    @return {Function|null}
666
    @protected
667
    **/
668
    _setResultHighlighter: function (highlighter) {
669
        var acHighlighters;
670
 
671
        if (this[_FUNCTION_VALIDATOR](highlighter)) {
672
            return highlighter;
673
        }
674
 
675
        acHighlighters = Y.AutoCompleteHighlighters;
676
 
677
        if (isString(highlighter) && acHighlighters &&
678
                isFunction(acHighlighters[highlighter])) {
679
            return acHighlighters[highlighter];
680
        }
681
 
682
        return INVALID_VALUE;
683
    },
684
 
685
    /**
686
    Setter for the `source` attribute. Returns a DataSource or a DataSource-like
687
    object depending on the type of _source_ and/or the value of the
688
    `sourceType` attribute.
689
 
690
    @method _setSource
691
    @param {Any} source AutoComplete source. See the `source` attribute for
692
        details.
693
    @return {DataSource|Object}
694
    @protected
695
    **/
696
    _setSource: function (source) {
697
        var sourceType = this.get('sourceType') || Lang.type(source),
698
            sourceSetter;
699
 
700
        if ((source && isFunction(source.sendRequest))
701
                || source === null
702
                || sourceType === 'datasource') {
703
 
704
            // Quacks like a DataSource instance (or null). Make it so!
705
            this._rawSource = source;
706
            return source;
707
        }
708
 
709
        // See if there's a registered setter for this source type.
710
        if ((sourceSetter = AutoCompleteBase.SOURCE_TYPES[sourceType])) {
711
            this._rawSource = source;
712
            return Lang.isString(sourceSetter) ?
713
                    this[sourceSetter](source) : sourceSetter(source);
714
        }
715
 
716
        Y.error("Unsupported source type '" + sourceType + "'. Maybe autocomplete-sources isn't loaded?");
717
        return INVALID_VALUE;
718
    },
719
 
720
    /**
721
    Shared success callback for non-DataSource sources.
722
 
723
    @method _sourceSuccess
724
    @param {Any} data Response data.
725
    @param {Object} request Request object.
726
    @protected
727
    **/
728
    _sourceSuccess: function (data, request) {
729
        request.callback.success({
730
            data: data,
731
            response: {results: data}
732
        });
733
    },
734
 
735
    /**
736
    Synchronizes the UI state of the `allowBrowserAutocomplete` attribute.
737
 
738
    @method _syncBrowserAutocomplete
739
    @protected
740
    **/
741
    _syncBrowserAutocomplete: function () {
742
        var inputNode = this.get(INPUT_NODE);
743
 
744
        if (inputNode.get('nodeName').toLowerCase() === 'input') {
745
            inputNode.setAttribute('autocomplete',
746
                    this.get(ALLOW_BROWSER_AC) ? 'on' : 'off');
747
        }
748
    },
749
 
750
    /**
751
    Updates the query portion of the `value` attribute.
752
 
753
    If a query delimiter is defined, the last delimited portion of the input
754
    value will be replaced with the specified _value_.
755
 
756
    @method _updateValue
757
    @param {String} newVal New value.
758
    @protected
759
    **/
760
    _updateValue: function (newVal) {
761
        var delim = this.get(QUERY_DELIMITER),
762
            insertDelim,
763
            len,
764
            prevVal;
765
 
766
        newVal = Lang.trimLeft(newVal);
767
 
768
        if (delim) {
769
            insertDelim = trim(delim); // so we don't double up on spaces
770
            prevVal     = YArray.map(trim(this.get(VALUE)).split(delim), trim);
771
            len         = prevVal.length;
772
 
773
            if (len > 1) {
774
                prevVal[len - 1] = newVal;
775
                newVal = prevVal.join(insertDelim + ' ');
776
            }
777
 
778
            newVal = newVal + insertDelim + ' ';
779
        }
780
 
781
        this.set(VALUE, newVal);
782
    },
783
 
784
    // -- Protected Event Handlers ---------------------------------------------
785
 
786
    /**
787
    Updates the current `source` based on the new `sourceType` to ensure that
788
    the two attributes don't get out of sync when they're changed separately.
789
 
790
    @method _afterSourceTypeChange
791
    @param {EventFacade} e
792
    @protected
793
    **/
794
    _afterSourceTypeChange: function (e) {
795
        if (this._rawSource) {
796
            this.set('source', this._rawSource);
797
        }
798
    },
799
 
800
    /**
801
    Handles change events for the `value` attribute.
802
 
803
    @method _afterValueChange
804
    @param {EventFacade} e
805
    @protected
806
    **/
807
    _afterValueChange: function (e) {
808
        var newVal   = e.newVal,
809
            self     = this,
810
            uiChange = e.src === AutoCompleteBase.UI_SRC,
811
            delay, fire, minQueryLength, query;
812
 
813
        // Update the UI if the value was changed programmatically.
814
        if (!uiChange) {
815
            self._inputNode.set(VALUE, newVal);
816
        }
817
 
818
 
819
        minQueryLength = self.get('minQueryLength');
820
        query          = self._parseValue(newVal) || '';
821
 
822
        if (minQueryLength >= 0 && query.length >= minQueryLength) {
823
            // Only query on changes that originate from the UI.
824
            if (uiChange) {
825
                delay = self.get('queryDelay');
826
 
827
                fire = function () {
828
                    self.fire(EVT_QUERY, {
829
                        inputValue: newVal,
830
                        query     : query,
831
                        src       : e.src
832
                    });
833
                };
834
 
835
                if (delay) {
836
                    clearTimeout(self._delay);
837
                    self._delay = setTimeout(fire, delay);
838
                } else {
839
                    fire();
840
                }
841
            } else {
842
                // For programmatic value changes, just update the query
843
                // attribute without sending a query.
844
                self._set(QUERY, query);
845
            }
846
        } else {
847
            clearTimeout(self._delay);
848
 
849
            self.fire(EVT_CLEAR, {
850
                prevVal: e.prevVal ? self._parseValue(e.prevVal) : null,
851
                src    : e.src
852
            });
853
        }
854
    },
855
 
856
    /**
857
    Handles `blur` events on the input node.
858
 
859
    @method _onInputBlur
860
    @param {EventFacade} e
861
    @protected
862
    **/
863
    _onInputBlur: function (e) {
864
        var delim = this.get(QUERY_DELIMITER),
865
            delimPos,
866
            newVal,
867
            value;
868
 
869
        // If a query delimiter is set and the input's value contains one or
870
        // more trailing delimiters, strip them.
871
        if (delim && !this.get('allowTrailingDelimiter')) {
872
            delim = Lang.trimRight(delim);
873
            value = newVal = this._inputNode.get(VALUE);
874
 
875
            if (delim) {
876
                while ((newVal = Lang.trimRight(newVal)) &&
877
                        (delimPos = newVal.length - delim.length) &&
878
                        newVal.lastIndexOf(delim) === delimPos) {
879
 
880
                    newVal = newVal.substring(0, delimPos);
881
                }
882
            } else {
883
                // Delimiter is one or more space characters, so just trim the
884
                // value.
885
                newVal = Lang.trimRight(newVal);
886
            }
887
 
888
            if (newVal !== value) {
889
                this.set(VALUE, newVal);
890
            }
891
        }
892
    },
893
 
894
    /**
895
    Handles `valueChange` events on the input node and fires a `query` event
896
    when the input value meets the configured criteria.
897
 
898
    @method _onInputValueChange
899
    @param {EventFacade} e
900
    @protected
901
    **/
902
    _onInputValueChange: function (e) {
903
        var newVal = e.newVal;
904
 
905
        // Don't query if the internal value is the same as the new value
906
        // reported by valueChange.
907
        if (newVal !== this.get(VALUE)) {
908
            this.set(VALUE, newVal, {src: AutoCompleteBase.UI_SRC});
909
        }
910
    },
911
 
912
    /**
913
    Handles source responses and fires the `results` event.
914
 
915
    @method _onResponse
916
    @param {EventFacade} e
917
    @protected
918
    **/
919
    _onResponse: function (query, e) {
920
        // Ignore stale responses that aren't for the current query.
921
        if (query === (this.get(QUERY) || '')) {
922
            this._parseResponse(query || '', e.response, e.data);
923
        }
924
    },
925
 
926
    // -- Protected Default Event Handlers -------------------------------------
927
 
928
    /**
929
    Default `clear` event handler. Sets the `results` attribute to an empty
930
    array and `query` to null.
931
 
932
    @method _defClearFn
933
    @protected
934
    **/
935
    _defClearFn: function () {
936
        this._set(QUERY, null);
937
        this._set(RESULTS, []);
938
    },
939
 
940
    /**
941
    Default `query` event handler. Sets the `query` attribute and sends a
942
    request to the source if one is configured.
943
 
944
    @method _defQueryFn
945
    @param {EventFacade} e
946
    @protected
947
    **/
948
    _defQueryFn: function (e) {
949
        this.sendRequest(e.query); // sendRequest will set the 'query' attribute
950
    },
951
 
952
    /**
953
    Default `results` event handler. Sets the `results` attribute to the latest
954
    results.
955
 
956
    @method _defResultsFn
957
    @param {EventFacade} e
958
    @protected
959
    **/
960
    _defResultsFn: function (e) {
961
        this._set(RESULTS, e[RESULTS]);
962
    }
963
};
964
 
965
AutoCompleteBase.ATTRS = {
966
    /**
967
    Whether or not to enable the browser's built-in autocomplete functionality
968
    for input fields.
969
 
970
    @attribute allowBrowserAutocomplete
971
    @type Boolean
972
    @default false
973
    **/
974
    allowBrowserAutocomplete: {
975
        value: false
976
    },
977
 
978
    /**
979
    When a `queryDelimiter` is set, trailing delimiters will automatically be
980
    stripped from the input value by default when the input node loses focus.
981
    Set this to `true` to allow trailing delimiters.
982
 
983
    @attribute allowTrailingDelimiter
984
    @type Boolean
985
    @default false
986
    **/
987
    allowTrailingDelimiter: {
988
        value: false
989
    },
990
 
991
    /**
992
    Whether or not to enable in-memory caching in result sources that support
993
    it.
994
 
995
    @attribute enableCache
996
    @type Boolean
997
    @default true
998
    @since 3.5.0
999
    **/
1000
    enableCache: {
1001
        lazyAdd: false, // we need the setter to run on init
1002
        setter: '_setEnableCache',
1003
        value: true
1004
    },
1005
 
1006
    /**
1007
    Node to monitor for changes, which will generate `query` events when
1008
    appropriate. May be either an `<input>` or a `<textarea>`.
1009
 
1010
    @attribute inputNode
1011
    @type Node|HTMLElement|String
1012
    @initOnly
1013
    **/
1014
    inputNode: {
1015
        setter: Y.one,
1016
        writeOnce: 'initOnly'
1017
    },
1018
 
1019
    /**
1020
    Maximum number of results to return. A value of `0` or less will allow an
1021
    unlimited number of results.
1022
 
1023
    @attribute maxResults
1024
    @type Number
1025
    @default 0
1026
    **/
1027
    maxResults: {
1028
        value: 0
1029
    },
1030
 
1031
    /**
1032
    Minimum number of characters that must be entered before a `query` event
1033
    will be fired. A value of `0` allows empty queries; a negative value will
1034
    effectively disable all `query` events.
1035
 
1036
    @attribute minQueryLength
1037
    @type Number
1038
    @default 1
1039
    **/
1040
    minQueryLength: {
1041
        value: 1
1042
    },
1043
 
1044
    /**
1045
    Current query, or `null` if there is no current query.
1046
 
1047
    The query might not be the same as the current value of the input node, both
1048
    for timing reasons (due to `queryDelay`) and because when one or more
1049
    `queryDelimiter` separators are in use, only the last portion of the
1050
    delimited input string will be used as the query value.
1051
 
1052
    @attribute query
1053
    @type String|null
1054
    @default null
1055
    @readonly
1056
    **/
1057
    query: {
1058
        readOnly: true,
1059
        value: null
1060
    },
1061
 
1062
    /**
1063
    Number of milliseconds to delay after input before triggering a `query`
1064
    event. If new input occurs before this delay is over, the previous input
1065
    event will be ignored and a new delay will begin.
1066
 
1067
    This can be useful both to throttle queries to a remote data source and to
1068
    avoid distracting the user by showing them less relevant results before
1069
    they've paused their typing.
1070
 
1071
    @attribute queryDelay
1072
    @type Number
1073
    @default 100
1074
    **/
1075
    queryDelay: {
1076
        value: 100
1077
    },
1078
 
1079
    /**
1080
    Query delimiter string. When a delimiter is configured, the input value
1081
    will be split on the delimiter, and only the last portion will be used in
1082
    autocomplete queries and updated when the `query` attribute is
1083
    modified.
1084
 
1085
    @attribute queryDelimiter
1086
    @type String|null
1087
    @default null
1088
    **/
1089
    queryDelimiter: {
1090
        value: null
1091
    },
1092
 
1093
    /**
1094
    Source request template. This can be a function that accepts a query as a
1095
    parameter and returns a request string, or it can be a string containing the
1096
    placeholder "{query}", which will be replaced with the actual URI-encoded
1097
    query. In either case, the resulting string will be appended to the request
1098
    URL when the `source` attribute is set to a remote DataSource, JSONP URL, or
1099
    XHR URL (it will not be appended to YQL URLs).
1100
 
1101
    While `requestTemplate` may be set to either a function or a string, it will
1102
    always be returned as a function that accepts a query argument and returns a
1103
    string.
1104
 
1105
    @attribute requestTemplate
1106
    @type Function|String|null
1107
    @default null
1108
    **/
1109
    requestTemplate: {
1110
        setter: '_setRequestTemplate',
1111
        value: null
1112
    },
1113
 
1114
    /**
1115
    Array of local result filter functions. If provided, each filter will be
1116
    called with two arguments when results are received: the query and an array
1117
    of result objects. See the documentation for the `results` event for a list
1118
    of the properties available on each result object.
1119
 
1120
    Each filter is expected to return a filtered or modified version of the
1121
    results array, which will then be passed on to subsequent filters, then the
1122
    `resultHighlighter` function (if set), then the `resultFormatter` function
1123
    (if set), and finally to subscribers to the `results` event.
1124
 
1125
    If no `source` is set, result filters will not be called.
1126
 
1127
    Prepackaged result filters provided by the autocomplete-filters and
1128
    autocomplete-filters-accentfold modules can be used by specifying the filter
1129
    name as a string, such as `'phraseMatch'` (assuming the necessary filters
1130
    module is loaded).
1131
 
1132
    @attribute resultFilters
1133
    @type Array
1134
    @default []
1135
    **/
1136
    resultFilters: {
1137
        setter: '_setResultFilters',
1138
        value: []
1139
    },
1140
 
1141
    /**
1142
    Function which will be used to format results. If provided, this function
1143
    will be called with two arguments after results have been received and
1144
    filtered: the query and an array of result objects. The formatter is
1145
    expected to return an array of HTML strings or Node instances containing the
1146
    desired HTML for each result.
1147
 
1148
    See the documentation for the `results` event for a list of the properties
1149
    available on each result object.
1150
 
1151
    If no `source` is set, the formatter will not be called.
1152
 
1153
    @attribute resultFormatter
1154
    @type Function|null
1155
    **/
1156
    resultFormatter: {
1157
        validator: _FUNCTION_VALIDATOR,
1158
        value: null
1159
    },
1160
 
1161
    /**
1162
    Function which will be used to highlight results. If provided, this function
1163
    will be called with two arguments after results have been received and
1164
    filtered: the query and an array of filtered result objects. The highlighter
1165
    is expected to return an array of highlighted result text in the form of
1166
    HTML strings.
1167
 
1168
    See the documentation for the `results` event for a list of the properties
1169
    available on each result object.
1170
 
1171
    If no `source` is set, the highlighter will not be called.
1172
 
1173
    @attribute resultHighlighter
1174
    @type Function|null
1175
    **/
1176
    resultHighlighter: {
1177
        setter: '_setResultHighlighter',
1178
        value: null
1179
    },
1180
 
1181
    /**
1182
    Locator that should be used to extract an array of results from a non-array
1183
    response.
1184
 
1185
    By default, no locator is applied, and all responses are assumed to be
1186
    arrays by default. If all responses are already arrays, you don't need to
1187
    define a locator.
1188
 
1189
    The locator may be either a function (which will receive the raw response as
1190
    an argument and must return an array) or a string representing an object
1191
    path, such as "foo.bar.baz" (which would return the value of
1192
    `result.foo.bar.baz` if the response is an object).
1193
 
1194
    While `resultListLocator` may be set to either a function or a string, it
1195
    will always be returned as a function that accepts a response argument and
1196
    returns an array.
1197
 
1198
    @attribute resultListLocator
1199
    @type Function|String|null
1200
    **/
1201
    resultListLocator: {
1202
        setter: '_setLocator',
1203
        value: null
1204
    },
1205
 
1206
    /**
1207
    Current results, or an empty array if there are no results.
1208
 
1209
    @attribute results
1210
    @type Array
1211
    @default []
1212
    @readonly
1213
    **/
1214
    results: {
1215
        readOnly: true,
1216
        value: []
1217
    },
1218
 
1219
    /**
1220
    Locator that should be used to extract a plain text string from a non-string
1221
    result item. The resulting text value will typically be the value that ends
1222
    up being inserted into an input field or textarea when the user of an
1223
    autocomplete implementation selects a result.
1224
 
1225
    By default, no locator is applied, and all results are assumed to be plain
1226
    text strings. If all results are already plain text strings, you don't need
1227
    to define a locator.
1228
 
1229
    The locator may be either a function (which will receive the raw result as
1230
    an argument and must return a string) or a string representing an object
1231
    path, such as "foo.bar.baz" (which would return the value of
1232
    `result.foo.bar.baz` if the result is an object).
1233
 
1234
    While `resultTextLocator` may be set to either a function or a string, it
1235
    will always be returned as a function that accepts a result argument and
1236
    returns a string.
1237
 
1238
    @attribute resultTextLocator
1239
    @type Function|String|null
1240
    **/
1241
    resultTextLocator: {
1242
        setter: '_setLocator',
1243
        value: null
1244
    },
1245
 
1246
    /**
1247
    Source for autocomplete results. The following source types are supported:
1248
 
1249
    <dl>
1250
      <dt>Array</dt>
1251
      <dd>
1252
        <p>
1253
        The full array will be provided to any configured filters for each
1254
        query. This is an easy way to create a fully client-side autocomplete
1255
        implementation.
1256
        </p>
1257
 
1258
        <p>
1259
        Example: `['first result', 'second result', 'etc']`
1260
        </p>
1261
      </dd>
1262
 
1263
      <dt>DataSource</dt>
1264
      <dd>
1265
        A `DataSource` instance or other object that provides a DataSource-like
1266
        `sendRequest` method. See the `DataSource` documentation for details.
1267
      </dd>
1268
 
1269
      <dt>Function</dt>
1270
      <dd>
1271
        <p>
1272
        A function source will be called with the current query and a
1273
        callback function as parameters, and should either return an array of
1274
        results (for synchronous operation) or return nothing and pass an
1275
        array of results to the provided callback (for asynchronous
1276
        operation).
1277
        </p>
1278
 
1279
        <p>
1280
        Example (synchronous):
1281
        </p>
1282
 
1283
        <pre>
1284
        function (query) {
1285
            return ['foo', 'bar'];
1286
        }
1287
        </pre>
1288
 
1289
        <p>
1290
        Example (async):
1291
        </p>
1292
 
1293
        <pre>
1294
        function (query, callback) {
1295
            callback(['foo', 'bar']);
1296
        }
1297
        </pre>
1298
      </dd>
1299
 
1300
      <dt>Object</dt>
1301
      <dd>
1302
        <p>
1303
        An object will be treated as a query hashmap. If a property on the
1304
        object matches the current query, the value of that property will be
1305
        used as the response.
1306
        </p>
1307
 
1308
        <p>
1309
        The response is assumed to be an array of results by default. If the
1310
        response is not an array, provide a `resultListLocator` to
1311
        process the response and return an array.
1312
        </p>
1313
 
1314
        <p>
1315
        Example: `{foo: ['foo result 1', 'foo result 2'], bar: ['bar result']}`
1316
        </p>
1317
      </dd>
1318
    </dl>
1319
 
1320
    If the optional `autocomplete-sources` module is loaded, then
1321
    the following additional source types will be supported as well:
1322
 
1323
    <dl>
1324
      <dt>&lt;select&gt; Node</dt>
1325
      <dd>
1326
        You may provide a YUI Node instance wrapping a &lt;select&gt;
1327
        element, and the options in the list will be used as results. You
1328
        will also need to specify a `resultTextLocator` of 'text'
1329
        or 'value', depending on what you want to use as the text of the
1330
        result.
1331
 
1332
        Each result will be an object with the following properties:
1333
 
1334
        <dl>
1335
          <dt>html (String)</dt>
1336
          <dd>
1337
            <p>HTML content of the &lt;option&gt; element.</p>
1338
          </dd>
1339
 
1340
          <dt>index (Number)</dt>
1341
          <dd>
1342
            <p>Index of the &lt;option&gt; element in the list.</p>
1343
          </dd>
1344
 
1345
          <dt>node (Y.Node)</dt>
1346
          <dd>
1347
            <p>Node instance referring to the original &lt;option&gt; element.</p>
1348
          </dd>
1349
 
1350
          <dt>selected (Boolean)</dt>
1351
          <dd>
1352
            <p>Whether or not this item is currently selected in the
1353
            &lt;select&gt; list.</p>
1354
          </dd>
1355
 
1356
          <dt>text (String)</dt>
1357
          <dd>
1358
            <p>Text content of the &lt;option&gt; element.</p>
1359
          </dd>
1360
 
1361
          <dt>value (String)</dt>
1362
          <dd>
1363
            <p>Value of the &lt;option&gt; element.</p>
1364
          </dd>
1365
        </dl>
1366
      </dd>
1367
 
1368
      <dt>String (JSONP URL)</dt>
1369
      <dd>
1370
        <p>
1371
        If a URL with a `{callback}` placeholder is provided, it will be used to
1372
        make a JSONP request. The `{query}` placeholder will be replaced with
1373
        the current query, and the `{callback}` placeholder will be replaced
1374
        with an internally-generated JSONP callback name. Both placeholders must
1375
        appear in the URL, or the request will fail. An optional `{maxResults}`
1376
        placeholder may also be provided, and will be replaced with the value of
1377
        the maxResults attribute (or 1000 if the maxResults attribute is 0 or
1378
        less).
1379
        </p>
1380
 
1381
        <p>
1382
        The response is assumed to be an array of results by default. If the
1383
        response is not an array, provide a `resultListLocator` to process the
1384
        response and return an array.
1385
        </p>
1386
 
1387
        <p>
1388
        <strong>The `jsonp` module must be loaded in order for
1389
        JSONP URL sources to work.</strong> If the `jsonp` module
1390
        is not already loaded, it will be loaded on demand if possible.
1391
        </p>
1392
 
1393
        <p>
1394
        Example: `'http://example.com/search?q={query}&callback={callback}'`
1395
        </p>
1396
      </dd>
1397
 
1398
      <dt>String (XHR URL)</dt>
1399
      <dd>
1400
        <p>
1401
        If a URL without a `{callback}` placeholder is provided, it will be used
1402
        to make a same-origin XHR request. The `{query}` placeholder will be
1403
        replaced with the current query. An optional `{maxResults}` placeholder
1404
        may also be provided, and will be replaced with the value of the
1405
        maxResults attribute (or 1000 if the maxResults attribute is 0 or less).
1406
        </p>
1407
 
1408
        <p>
1409
        The response is assumed to be a JSON array of results by default. If the
1410
        response is a JSON object and not an array, provide a
1411
        `resultListLocator` to process the response and return an array. If the
1412
        response is in some form other than JSON, you will need to use a custom
1413
        DataSource instance as the source.
1414
        </p>
1415
 
1416
        <p>
1417
        <strong>The `io-base` and `json-parse` modules
1418
        must be loaded in order for XHR URL sources to work.</strong> If
1419
        these modules are not already loaded, they will be loaded on demand
1420
        if possible.
1421
        </p>
1422
 
1423
        <p>
1424
        Example: `'http://example.com/search?q={query}'`
1425
        </p>
1426
      </dd>
1427
 
1428
      <dt>String (YQL query)</dt>
1429
      <dd>
1430
        <p>
1431
        If a YQL query is provided, it will be used to make a YQL request. The
1432
        `{query}` placeholder will be replaced with the current autocomplete
1433
        query. This placeholder must appear in the YQL query, or the request
1434
        will fail. An optional `{maxResults}` placeholder may also be provided,
1435
        and will be replaced with the value of the maxResults attribute (or 1000
1436
        if the maxResults attribute is 0 or less).
1437
        </p>
1438
 
1439
        <p>
1440
        <strong>The `yql` module must be loaded in order for YQL
1441
        sources to work.</strong> If the `yql` module is not
1442
        already loaded, it will be loaded on demand if possible.
1443
        </p>
1444
 
1445
        <p>
1446
        Example: `'select * from search.suggest where query="{query}"'`
1447
        </p>
1448
      </dd>
1449
    </dl>
1450
 
1451
    As an alternative to providing a source, you could simply listen for `query`
1452
    events and handle them any way you see fit. Providing a source is optional,
1453
    but will usually be simpler.
1454
 
1455
    @attribute source
1456
    @type Array|DataSource|Function|Node|Object|String|null
1457
    **/
1458
    source: {
1459
        setter: '_setSource',
1460
        value: null
1461
    },
1462
 
1463
    /**
1464
    May be used to force a specific source type, overriding the automatic source
1465
    type detection. It should almost never be necessary to do this, but as they
1466
    taught us in the Boy Scouts, one should always be prepared, so it's here if
1467
    you need it. Be warned that if you set this attribute and something breaks,
1468
    it's your own fault.
1469
 
1470
    Supported `sourceType` values are: 'array', 'datasource', 'function', and
1471
    'object'.
1472
 
1473
    If the `autocomplete-sources` module is loaded, the following additional
1474
    source types are supported: 'io', 'jsonp', 'select', 'string', 'yql'
1475
 
1476
    @attribute sourceType
1477
    @type String
1478
    **/
1479
    sourceType: {
1480
        value: null
1481
    },
1482
 
1483
    /**
1484
    If the `inputNode` specified at instantiation time has a `node-tokeninput`
1485
    plugin attached to it, this attribute will be a reference to the
1486
    `Y.Plugin.TokenInput` instance.
1487
 
1488
    @attribute tokenInput
1489
    @type Plugin.TokenInput
1490
    @readonly
1491
    **/
1492
    tokenInput: {
1493
        readOnly: true
1494
    },
1495
 
1496
    /**
1497
    Current value of the input node.
1498
 
1499
    @attribute value
1500
    @type String
1501
    @default ''
1502
    **/
1503
    value: {
1504
        // Why duplicate this._inputNode.get('value')? Because we need a
1505
        // reliable way to track the source of value changes. We want to perform
1506
        // completion when the user changes the value, but not when we change
1507
        // the value.
1508
        value: ''
1509
    }
1510
};
1511
 
1512
// This tells Y.Base.create() to copy these static properties to any class
1513
// AutoCompleteBase is mixed into.
1514
AutoCompleteBase._buildCfg = {
1515
    aggregates: ['SOURCE_TYPES'],
1516
    statics   : ['UI_SRC']
1517
};
1518
 
1519
/**
1520
Mapping of built-in source types to their setter functions. DataSource instances
1521
and DataSource-like objects are handled natively, so are not mapped here.
1522
 
1523
@property SOURCE_TYPES
1524
@type {Object}
1525
@static
1526
**/
1527
AutoCompleteBase.SOURCE_TYPES = {
1528
    array     : '_createArraySource',
1529
    'function': '_createFunctionSource',
1530
    object    : '_createObjectSource'
1531
};
1532
 
1533
AutoCompleteBase.UI_SRC = (Y.Widget && Y.Widget.UI_SRC) || 'ui';
1534
 
1535
Y.AutoCompleteBase = AutoCompleteBase;
1536
 
1537
 
1538
}, '3.18.1', {
1539
    "optional": [
1540
        "autocomplete-sources"
1541
    ],
1542
    "requires": [
1543
        "array-extras",
1544
        "base-build",
1545
        "escape",
1546
        "event-valuechange",
1547
        "node-base"
1548
    ]
1549
});