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