UrlbarResult.sys.mjs (13770B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This module exports a urlbar result class, each representing a single result 7 * found by a provider that can be passed from the model to the view through 8 * the controller. It is mainly defined by a result type, and a payload, 9 * containing the data. A few getters allow to retrieve information common to all 10 * the result types. 11 */ 12 13 const lazy = {}; 14 15 ChromeUtils.defineESModuleGetters(lazy, { 16 JsonSchemaValidator: 17 "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs", 18 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 19 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 20 }); 21 22 /** 23 * @typedef UrlbarAutofillData 24 * @property {string} value 25 * The value to insert for autofill. 26 * @property {number} selectionStart 27 * Where to start the selection for the autofill. 28 * @property {number} selectionEnd 29 * Where to end the selection for the autofill. 30 * @property {string} [type] 31 * The type of the autofill. 32 * @property {string} [adaptiveHistoryInput] 33 * The input string associated with this autofill item. 34 */ 35 36 /** 37 * Class used to create a single result. 38 */ 39 export class UrlbarResult { 40 /** 41 * @typedef {{ [name: string]: any }} Payload 42 * 43 * @typedef {typeof lazy.UrlbarUtils.HIGHLIGHT} HighlightType 44 * @typedef {Array<[number, number]>} HighlightIndexes e.g. [[index, length],,] 45 * @typedef {Record<string, HighlightType | HighlightIndexes>} Highlights 46 */ 47 48 /** 49 * @param {object} params 50 * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} params.type 51 * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} params.source 52 * @param {UrlbarAutofillData} [params.autofill] 53 * @param {number} [params.exposureTelemetry] 54 * @param {Values<typeof lazy.UrlbarUtils.RESULT_GROUP>} [params.group] 55 * @param {boolean} [params.heuristic] 56 * @param {boolean} [params.hideRowLabel] 57 * @param {boolean} [params.isBestMatch] 58 * @param {boolean} [params.isRichSuggestion] 59 * @param {boolean} [params.isSuggestedIndexRelativeToGroup] 60 * @param {string} [params.providerName] 61 * @param {number} [params.resultSpan] 62 * @param {number} [params.richSuggestionIconSize] 63 * @param {string} [params.richSuggestionIconVariation] 64 * @param {string} [params.rowLabel] 65 * @param {boolean} [params.showFeedbackMenu] 66 * @param {number} [params.suggestedIndex] 67 * @param {Payload} [params.payload] 68 * @param {Highlights} [params.highlights] 69 * @param {boolean} [params.testForceNewContent] Used for test only. 70 */ 71 constructor({ 72 type, 73 source, 74 autofill, 75 exposureTelemetry = lazy.UrlbarUtils.EXPOSURE_TELEMETRY.NONE, 76 group, 77 heuristic = false, 78 hideRowLabel = false, 79 isBestMatch = false, 80 isRichSuggestion = false, 81 isSuggestedIndexRelativeToGroup = false, 82 providerName, 83 resultSpan, 84 richSuggestionIconSize, 85 richSuggestionIconVariation, 86 rowLabel, 87 showFeedbackMenu = false, 88 suggestedIndex, 89 payload, 90 highlights = null, 91 testForceNewContent, 92 }) { 93 // Type describes the payload and visualization that should be used for 94 // this result. 95 if (!Object.values(lazy.UrlbarUtils.RESULT_TYPE).includes(type)) { 96 throw new Error("Invalid result type"); 97 } 98 this.#type = type; 99 100 // Source describes which data has been used to derive this result. In case 101 // multiple sources are involved, use the more privacy restricted. 102 if (!Object.values(lazy.UrlbarUtils.RESULT_SOURCE).includes(source)) { 103 throw new Error("Invalid result source"); 104 } 105 this.#source = source; 106 107 // The payload contains result data. Some of the data is common across 108 // multiple types, but most of it will vary. 109 if (!payload || typeof payload != "object") { 110 throw new Error("Invalid result payload"); 111 } 112 113 payload = Object.fromEntries( 114 Object.entries(payload).filter(([_, v]) => v != undefined) 115 ); 116 117 if (highlights) { 118 this.#highlights = Object.freeze(highlights); 119 } 120 121 this.#payload = this.#validatePayload(payload); 122 123 this.#autofill = autofill; 124 this.#exposureTelemetry = exposureTelemetry; 125 this.#group = group; 126 this.#heuristic = heuristic; 127 this.#hideRowLabel = hideRowLabel; 128 this.#isBestMatch = isBestMatch; 129 this.#isRichSuggestion = isRichSuggestion; 130 this.#isSuggestedIndexRelativeToGroup = isSuggestedIndexRelativeToGroup; 131 this.#richSuggestionIconSize = richSuggestionIconSize; 132 this.#richSuggestionIconVariation = richSuggestionIconVariation; 133 this.#providerName = providerName; 134 this.#resultSpan = resultSpan; 135 this.#rowLabel = rowLabel; 136 this.#showFeedbackMenu = showFeedbackMenu; 137 this.#suggestedIndex = suggestedIndex; 138 139 if (this.#type == lazy.UrlbarUtils.RESULT_TYPE.TIP) { 140 this.#isRichSuggestion = true; 141 this.#richSuggestionIconSize = 24; 142 } 143 144 this.#testForceNewContent = testForceNewContent; 145 } 146 147 /** 148 * @type {number} 149 * The index of the row where this result is in the suggestions. This is 150 * updated by UrlbarView when new result sets are displayed. 151 */ 152 rowIndex = undefined; 153 154 get type() { 155 return this.#type; 156 } 157 158 get source() { 159 return this.#source; 160 } 161 162 get autofill() { 163 return this.#autofill; 164 } 165 166 get exposureTelemetry() { 167 return this.#exposureTelemetry; 168 } 169 set exposureTelemetry(value) { 170 this.#exposureTelemetry = value; 171 } 172 173 get group() { 174 return this.#group; 175 } 176 177 get heuristic() { 178 return this.#heuristic; 179 } 180 181 get hideRowLabel() { 182 return this.#hideRowLabel; 183 } 184 185 get isBestMatch() { 186 return this.#isBestMatch; 187 } 188 189 get isRichSuggestion() { 190 return this.#isRichSuggestion; 191 } 192 set isRichSuggestion(value) { 193 this.#isRichSuggestion = value; 194 } 195 196 get isSuggestedIndexRelativeToGroup() { 197 return this.#isSuggestedIndexRelativeToGroup; 198 } 199 set isSuggestedIndexRelativeToGroup(value) { 200 this.#isSuggestedIndexRelativeToGroup = value; 201 } 202 203 get providerName() { 204 return this.#providerName; 205 } 206 set providerName(value) { 207 this.#providerName = value; 208 } 209 210 /** 211 * The type of the UrlbarProvider providing the result. 212 * 213 * @type {?Values<typeof lazy.UrlbarUtils.PROVIDER_TYPE>} 214 */ 215 get providerType() { 216 return this.#providerType; 217 } 218 set providerType(value) { 219 this.#providerType = value; 220 } 221 222 get resultSpan() { 223 return this.#resultSpan; 224 } 225 226 get richSuggestionIconSize() { 227 return this.#richSuggestionIconSize; 228 } 229 230 get richSuggestionIconVariation() { 231 return this.#richSuggestionIconVariation; 232 } 233 set richSuggestionIconSize(value) { 234 this.#richSuggestionIconSize = value; 235 } 236 237 get rowLabel() { 238 return this.#rowLabel; 239 } 240 241 get showFeedbackMenu() { 242 return this.#showFeedbackMenu; 243 } 244 245 get suggestedIndex() { 246 return this.#suggestedIndex; 247 } 248 set suggestedIndex(value) { 249 this.#suggestedIndex = value; 250 } 251 252 get payload() { 253 return this.#payload; 254 } 255 256 get testForceNewContent() { 257 return this.#testForceNewContent; 258 } 259 260 /** 261 * Returns an icon url. 262 * 263 * @returns {string} url of the icon. 264 */ 265 get icon() { 266 return this.payload.icon; 267 } 268 269 /** 270 * Returns whether the result's `suggestedIndex` property is defined. 271 * `suggestedIndex` is an optional hint to the muxer that can be set to 272 * suggest a specific position among the results. 273 * 274 * @returns {boolean} Whether `suggestedIndex` is defined. 275 */ 276 get hasSuggestedIndex() { 277 return typeof this.suggestedIndex == "number"; 278 } 279 280 /** 281 * Convenience getter that returns whether the result's exposure telemetry 282 * indicates it should be hidden. 283 * 284 * @returns {boolean} 285 * Whether the result should be hidden. 286 */ 287 get isHiddenExposure() { 288 return this.exposureTelemetry == lazy.UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN; 289 } 290 291 /** 292 * Get value and highlights of given payloadName that can display in the view. 293 * 294 * @param {string} payloadName 295 * The payload name to want to get the value. 296 * @param {object} options 297 * @param {object} [options.tokens] 298 * Make highlighting that matches this tokens. 299 * If no specific tokens, this function returns only value. 300 * @param {object} [options.isURL] 301 * If true, the value will be from UrlbarUtils.prepareUrlForDisplay(). 302 */ 303 getDisplayableValueAndHighlights(payloadName, options = {}) { 304 if (!this.#displayValuesCache) { 305 this.#displayValuesCache = new Map(); 306 } 307 308 if (this.#displayValuesCache.has(payloadName)) { 309 let cached = this.#displayValuesCache.get(payloadName); 310 // If the different options are specified, ignore the cache. 311 // NOTE: If options.tokens is undefined, use cache as it is. 312 if ( 313 options.isURL == cached.options.isURL && 314 (options.tokens == undefined || 315 lazy.ObjectUtils.deepEqual(options.tokens, cached.options.tokens)) 316 ) { 317 return this.#displayValuesCache.get(payloadName); 318 } 319 } 320 321 let value = this.payload[payloadName]; 322 if (!value) { 323 return {}; 324 } 325 326 if (options.isURL) { 327 value = lazy.UrlbarUtils.prepareUrlForDisplay(value); 328 } 329 330 if (typeof value == "string") { 331 value = value.substring(0, lazy.UrlbarUtils.MAX_TEXT_LENGTH); 332 } 333 334 if (Array.isArray(this.#highlights?.[payloadName])) { 335 return { value, highlights: this.#highlights[payloadName] }; 336 } 337 338 let highlightType = this.#highlights?.[payloadName]; 339 340 if (!options.tokens?.length || !highlightType) { 341 let cached = { value, options }; 342 this.#displayValuesCache.set(payloadName, cached); 343 return cached; 344 } 345 346 let highlights = Array.isArray(value) 347 ? value.map(subval => 348 lazy.UrlbarUtils.getTokenMatches( 349 options.tokens, 350 subval, 351 highlightType 352 ) 353 ) 354 : lazy.UrlbarUtils.getTokenMatches(options.tokens, value, highlightType); 355 356 let cached = { value, highlights, options }; 357 this.#displayValuesCache.set(payloadName, cached); 358 return cached; 359 } 360 361 /** 362 * Returns the given payload if it's valid or throws an error if it's not. 363 * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation. 364 * 365 * @param {object} payload The payload object. 366 * @returns {object} `payload` if it's valid. 367 */ 368 #validatePayload(payload) { 369 let schema = lazy.UrlbarUtils.getPayloadSchema(this.type); 370 if (!schema) { 371 throw new Error(`Unrecognized result type: ${this.type}`); 372 } 373 let result = lazy.JsonSchemaValidator.validate(payload, schema, { 374 allowExplicitUndefinedProperties: true, 375 allowNullAsUndefinedProperties: true, 376 allowAdditionalProperties: 377 this.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC, 378 }); 379 if (!result.valid) { 380 throw result.error; 381 } 382 return payload; 383 } 384 385 static _dynamicResultTypesByName = new Map(); 386 387 /** 388 * Registers a dynamic result type. Dynamic result types are types that are 389 * created at runtime, for example by an extension. A particular type should 390 * be added only once; if this method is called for a type more than once, the 391 * `type` in the last call overrides those in previous calls. 392 * 393 * @param {string} name 394 * The name of the type. This is used in CSS selectors, so it shouldn't 395 * contain any spaces or punctuation except for -, _, etc. 396 * @param {object} type 397 * An object that describes the type. Currently types do not have any 398 * associated metadata, so this object should be empty. 399 */ 400 static addDynamicResultType(name, type = {}) { 401 if (/[^a-z0-9_-]/i.test(name)) { 402 console.error(`Illegal dynamic type name: ${name}`); 403 return; 404 } 405 this._dynamicResultTypesByName.set(name, type); 406 } 407 408 /** 409 * Unregisters a dynamic result type. 410 * 411 * @param {string} name 412 * The name of the type. 413 */ 414 static removeDynamicResultType(name) { 415 let type = this._dynamicResultTypesByName.get(name); 416 if (type) { 417 this._dynamicResultTypesByName.delete(name); 418 } 419 } 420 421 /** 422 * Returns an object describing a registered dynamic result type. 423 * 424 * @param {string} name 425 * The name of the type. 426 * @returns {object} 427 * Currently types do not have any associated metadata, so the return value 428 * is an empty object if the type exists. If the type doesn't exist, 429 * undefined is returned. 430 */ 431 static getDynamicResultType(name) { 432 return this._dynamicResultTypesByName.get(name); 433 } 434 435 /** 436 * This is useful for logging results. If you need the full payload, then it's 437 * better to JSON.stringify the result object itself. 438 * 439 * @returns {string} string representation of the result. 440 */ 441 toString() { 442 if (this.payload.url) { 443 return this.payload.title + " - " + this.payload.url.substr(0, 100); 444 } 445 if (this.payload.keyword) { 446 return this.payload.keyword + " - " + this.payload.query; 447 } 448 if (this.payload.suggestion) { 449 return this.payload.engine + " - " + this.payload.suggestion; 450 } 451 if (this.payload.engine) { 452 return this.payload.engine + " - " + this.payload.query; 453 } 454 return JSON.stringify(this); 455 } 456 457 #type; 458 #source; 459 #autofill; 460 #exposureTelemetry; 461 #group; 462 #heuristic; 463 #hideRowLabel; 464 #isBestMatch; 465 #isRichSuggestion; 466 #isSuggestedIndexRelativeToGroup; 467 #providerName; 468 #providerType; 469 #resultSpan; 470 #richSuggestionIconSize; 471 #richSuggestionIconVariation; 472 #rowLabel; 473 #showFeedbackMenu; 474 #suggestedIndex; 475 #payload; 476 #highlights; 477 #displayValuesCache; 478 #testForceNewContent; 479 }