PageSourceSearch

https://dev.rutoken.ru/includes/js/input-driven-dropdown.js

js rutoken.ru collected 2026-09-24 08:59:00 UTC 14,303 bytes, 395 lines download raw bytes

1/**
2 * @module confluence/input-driven-dropdown
3 */
4define('confluence/input-driven-dropdown', [
5    'ajs',
6    'jquery',
7    'confluence/highlighter',
8    'confluence/legacy',
9    'window',
10    'document'
11], function(
12    AJS,
13    $,
14    Highlighter,
15    Confluence,
16    window,
17    document
18) {
19    'use strict';
20
21    /**
22     * Check that all items in the drop down can be displayed - show ellipses at the end of any that
23     * are too long. Also remove any unused properties that the dropDown may have stored for each
24     * item in the list.
25     *
26     * @method truncateText
27     * @private
28     */
29    var truncateText = function(dd) {
30        var width = dd.$.closest('.aui-dd-parent').width();
31        var rightPadding = 20; // add some padding so the ellipsis doesn't run over the edge of the box
32
33        // If there is no width to calculate, default to 25em
34        if (!width) {
35            dd.$.width('25em');
36            width = dd.$.width();
37        }
38
39        $('a span:not(.icon)', dd.$).each(function() {
40            var $a = $(this);
41            var elpss = $('<var>&#8230;</var>');
42            var elwidth = elpss.width();
43            var isLong = false;
44
45            $a.wrapInner($('<em></em>'));
46            $('em', $a).each(function() {
47                var $label = $(this);
48
49                $label.show();
50                if (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding) {
51                    var childNodes = this.childNodes;
52                    var success = false;
53
54                    for (var j = childNodes.length - 1; j >= 0; j--) {
55                        var childNode = childNodes[j];
56                        var truncatedChars = 1;
57                        var valueAttr = (childNode.nodeType === 3) ? 'nodeValue' : 'innerHTML';
58                        var nodeText = childNode[valueAttr];
59
60                        do {
61                            if (truncatedChars <= nodeText.length) {
62                                childNode[valueAttr] = nodeText.substr(0, nodeText.length - truncatedChars++);
63                            } else { // if we cannot fit even one character of the next word, then try truncating the node just previous to this
64                                break;
65                            }
66                        } while (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding);
67
68                        if (truncatedChars <= nodeText.length) {
69                            // we've managed truncate part of the word and fit it in
70                            success = true;
71                            break;
72                        }
73                    }
74
75                    if (success) {
76                        isLong = true;
77                    } else {
78                        $label.hide();
79                    }
80                }
81            });
82            if (isLong) {
83                $a.append(elpss);
84                this.elpss = elpss;
85            }
86        });
87    };
88
89    var addSpaceNames = function(dd) {
90        $('a span:not(.icon)', dd.$).each(function() {
91            var $a = $(this);
92            // get the hidden space name property from the span
93            // need to surround with try/catch until AJS-294 resolved
94            var spaceName;
95            try {
96                var properties = $a.data('properties');
97                spaceName = properties ? properties.spaceName : null;
98            } catch (err) {
99                AJS.logError('Problem getting space name: ' + err.message);
100            }
101            // we need to go through html node creation so that all encoded symbols(like &gt;) are displayed correctly
102
103            var title = $a.text();
104            if (spaceName) {
105                title += ' (' + $('<i></i>').html(spaceName).text() + ')';
106            }
107
108            $a.attr('title', title);
109        });
110    };
111
112    /**
113     * Builds and shows the dropdown.
114     *
115     * @param idd the InputDrivenDropdown
116     * @param dropdownData
117     * @private
118     */
119    var makeDropdown = function(idd, dropdownData) {
120        var options = idd.options;
121        options.ajsDropDownOptions = options.ajsDropDownOptions || {};
122
123        var HighlighterCreator = options.ajsDropDownOptions.Highlighter || Highlighter;
124        var highlighter = HighlighterCreator(dropdownData.queryTokens || [dropdownData.query]);
125
126        if (!options.ajsDropDownOptions.alignment) { // default to left alignment
127            options.ajsDropDownOptions.alignment = 'left';
128        }
129        options.ajsDropDownOptions.selectionHandler = options.ajsDropDownOptions.selectionHandler || defaultSelectionHandler;
130
131        _hideOldDropdownIfPresent(idd);
132
133        var dd = idd.dd = AJS.dropDown(dropdownData.matrix, options.ajsDropDownOptions)[0];
134
135        if (options.ajsDropDownOptions.className) {
136            dd.$.addClass(options.ajsDropDownOptions.className);
137        }
138
139        dd.$.find('li a:not(.dropdown-prevent-highlight) span').each(function() {
140            var span = $(this);
141            span.html(highlighter.safeHighlight(span.text(), span.data().properties));
142        });
143
144        // For the Fabric-style dropdown, add the username and (optional) lozenge
145        // on a second line of each item. Note that the current implementation is
146        // specific for the @-Mention results - if we want to roll this pattern out
147        // we'll need to replace data.username with something more generic.
148        if (dd.$.hasClass('fabric')) {
149            var $mentions = dd.$.find('.insert-mentions-dropdown-option');
150            if ($mentions.length === dropdownData.matrix[1].length) {
151                dropdownData.matrix[1].forEach(function(userData, index) {
152                    var lozengeInner = highlighter.safeHighlight(userData.username, userData);
153                    if (!lozengeInner) {
154                        return;
155                    }
156
157                    if (userData.lozenge) {
158                        lozengeInner += ' &middot; ' + userData.lozenge;
159                    }
160                    var $span = $('<span></span>').addClass('dropdown-lozenge').html(lozengeInner);
161
162                    $mentions.eq(index).append($span);
163                });
164            }
165        }
166
167        // place the created drop down using the configured dropdownPlacement function
168        // if there is none then use a default behaviour
169        if (options.dropdownPlacement) {
170            options.dropdownPlacement(dd.$);
171        } else {
172            AJS.log('No dropdownPlacement function specified. Appending dropdown to the body.');
173            $('body').append(dd.$);
174        }
175
176        truncateText(dd);
177        addSpaceNames(dd);
178
179        if (options.dropdownPostprocess) {
180            options.dropdownPostprocess(dd.$);
181        }
182        dd.show(idd._effect);
183
184        if (typeof options.onShow === 'function') {
185            options.onShow.call(dd, dd.$);
186        }
187
188        return dd;
189    };
190
191    var defaultSelectionHandler = function(e, element) {
192        var windowOpened;
193        if (e.type == 'click') {
194            return;
195        }
196
197        var $firstAnchor = $('a', element).first();
198        var href = $firstAnchor.attr('href');
199        var ctrlEnterPressed = e.type === 'keydown' && (e.metaKey || e.ctrlKey) && e.which === 13;
200
201        // open selection in a new window when ctrl+enter has been pressed
202        if (ctrlEnterPressed) {
203            windowOpened = window.open(href, '_blank');
204            windowOpened.opener = null;
205        } else {
206            $firstAnchor.click();
207            document.location = href;
208        }
209
210        e.preventDefault();
211    };
212
213    /**
214     * If there is already an input driven dropdown on the screen, close it before rendering the new one
215     * @param inputDrivenDropdown
216     * @private
217     */
218    function _hideOldDropdownIfPresent(inputDrivenDropdown) {
219        var old_dropdown = inputDrivenDropdown.dd;
220
221        if (old_dropdown) {
222            old_dropdown.hide();
223            old_dropdown.$.remove();
224        }
225    }
226
227    /**
228     * Provides a controller-agnostic object that listens for controller changes and populates a dropdown
229     * via a callback. Most aspects can be customized via the options object parameter.
230     * <br>
231     * Options are:
232     * <li>
233     *   getDataAndRunCallback - (required) callback method used to provide data for the dropdown. It must take
234     *                          two parameters, user input value and the callback function to execute.
235     * </li>
236     * <li>
237     *   onShow - function to call when the drop-down is displayed
238     * </li>
239     * <li>
240     *   dropdownPlacement - a function that will be called with the drop down and which should place it in the
241     *                          correct place on the page. The supplied arguments are 1) the input that issued the
242     *                          search, 2) the dropDown to be placed.
243     * </li>
244     * <li>
245     *   ajsDropDownOptions - any options the underlying dropDown component can handle expects
246     * </li>
247     * <li>
248     *   onDeath - callback to run when dropdown dies
249     * </li>
250     * <li>
251     *   minLengthForGetData - a number to indicate the minimum length of a keyword to call
252     *                         options.getDataAndRunCallback. Example: if minLengthForGetData=2, and the keyword
253     *                         has only one character, it will not make an ajax request.
254     * </li>
255     * @class InputDrivenDropDown
256     * @namespace AJS
257     */
258    function InputDrivenDropDown(id, options) {
259        this._effect = 'appear';
260        this._timer = null;
261
262        this.id = id;
263        this.options = options;
264        this.inactive = false;
265        this.busy = false;
266        this.cacheManager = AJS.Confluence.cacheManager();
267    }
268
269    /**
270     * Clears the cache.
271     */
272    InputDrivenDropDown.prototype.clearCache = function() {
273        this.cacheManager.clear();
274    };
275
276    /**
277     * This method should be called when the user input for this dropdown has changed.
278     * It will check the cache before fetching data (via options.getDataAndRunCallback)
279     * and displaying the dropdown.
280     *
281     * @param value {String} the new value of the user input
282     * @param force {Boolean} force a change to occur regardless of user input
283     * @param finishCallback {Function} a method to call when the processing of the change is complete. It is passed
284     *                                  a single argument which is the keyword that finished being processed.
285     */
286    InputDrivenDropDown.prototype.change = function(value, force, finishCallback) {
287        var t = this;
288        if (value != t._value) {
289            t._value = value;
290            t.busy = false;
291
292            window.clearTimeout(t._timer);
293
294            var minLengthForGetData = t.options.minLengthForGetData || 2;
295            var reg = new RegExp('\\S{' + minLengthForGetData + ',}');
296
297            if (force || reg.test(value)) {
298                var cachedVal = t.cacheManager.get(value);
299                if (cachedVal) {
300                    makeDropdown(t, cachedVal);
301                } else {
302                    t.busy = true;
303                    var getDataAndRunCallback = function() {
304                        t.options.getDataAndRunCallback.call(t, value, function() {
305                            t.show.apply(this, arguments);
306                            finishCallback && finishCallback(value);
307                        });
308                    };
309
310                    if (t.options.dropDownDelay === 0) {
311                        getDataAndRunCallback();
312                    } else {
313                        t._timer = window.setTimeout(function() { // delay sending a request to give the user a chance to finish typing their search term(s)
314                            getDataAndRunCallback();
315                        }, t.options.dropDownDelay || 200);
316                    }
317                }
318            } else {
319                t.dd && t.dd.hide();
320                finishCallback && finishCallback(value);
321            }
322        }
323    };
324
325    /**
326     * Hides the drop down
327     */
328    InputDrivenDropDown.prototype.hide = function() {
329        this.dd && this.dd.hide();
330    };
331
332    /**
333     * Hides and removes the drop down from the DOM.
334     */
335    InputDrivenDropDown.prototype.remove = function() {
336        var dd = this.dd;
337        if (dd) {
338            this.hide();
339            dd.$.remove();
340        }
341        this.inactive = true;
342        this.options.onDeath && this.options.onDeath();
343    };
344
345    /**
346     * Shows the drop down with the given matrix data and query.
347     * <br>
348     * Matrix property should be an array of arrays, where the sub-arrays represent the different
349     * search categories.
350     *
351     * Expected properties of category sub-array objects are:
352     *  - href
353     *  - name
354     *  - className
355     *  - html (optional, replaces href and name)
356     *  - icon (optional)
357     *
358     *
359     * @param matrix {Array} matrix to populate the drop down from
360     * @param query {String} the user input string that triggered this show
361     * @param queryTokens {Array} an array of strings of the query tokens. Use for highlighting search terms.
362     */
363    InputDrivenDropDown.prototype.show = function(matrix, query, queryTokens) {
364        if (this.inactive) {
365            AJS.log('Quick search abandoned before server response received, ignoring. ' + this);
366            return;
367        }
368
369        var dropdownData = {
370            matrix: matrix,
371            query: query,
372            queryTokens: queryTokens
373        };
374        this.cacheManager.put(query, dropdownData);
375
376        makeDropdown(this, dropdownData);
377        this.busy = false;
378    };
379
380    return InputDrivenDropDown;
381});
382
383require('confluence/module-exporter')
384    .safeRequire('confluence/input-driven-dropdown', function(InputDrivenDropDown) {
385        'use strict';
386
387        /**
388         * Returns an InputDrivenDropDown. See InputDrivenDropDown for more documentation.
389         * @param options {Object} options for the InputDrivenDropDown
390         * @constructor
391         */
392        require('confluence/module-exporter').namespace('AJS.inputDrivenDropdown', function(options) {
393            return new InputDrivenDropDown('inputdriven-dropdown', options);
394        });
395    });

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.