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>…</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 >) 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 += ' · ' + 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.