blob: 7899d43960851b7410b808faadbed0623d669710 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2016 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5// ------------------------------------------------------------------------
6// This file contains common utilities and basic javascript infrastructure.
7//
8// Notes:
9// * Press 'D' to toggle debug mode.
10//
11// Functions:
12//
13// - Assertions
14// DEPRECATED: Use assert.js
15// AssertTrue(): assert an expression. Throws an exception if false.
16// Fail(): Throws an exception. (Mark block of code that should be unreachable)
17// AssertEquals(): assert that two values are equal.
18// AssertType(): assert that a value has a particular type
19//
20// - Cookies
21// SetCookie(): Sets a cookie.
22// ExpireCookie(): Expires a cookie.
23// GetCookie(): Gets a cookie value.
24//
25// - Dynamic HTML/DOM utilities
26// MaybeGetElement(): get an element by its id
27// GetElement(): get an element by its id
28// GetParentNode(): Get the parent of an element
29// GetAttribute(): Get attribute value of a DOM node
30// GetInnerHTML(): get the inner HTML of a node
31// SetCssStyle(): Sets a CSS property of a node.
32// GetStyleProperty(): Get CSS property from a style attribute string
33// GetCellIndex(): Get the index of a table cell in a table row
34// ShowElement(): Show/hide element by setting the "display" css property.
35// ShowBlockElement(): Show/hide block element
36// SetButtonText(): Set the text of a button element.
37// AppendNewElement(): Create and append a html element to a parent node.
38// CreateDIV(): Create a DIV element and append to the document.
39// HasClass(): check if element has a given class
40// AddClass(): add a class to an element
41// RemoveClass(): remove a class from an element
42//
43// - Window/Screen utiltiies
44// GetPageOffsetLeft(): get the X page offset of an element
45// GetPageOffsetTop(): get the Y page offset of an element
46// GetPageOffset(): get the X and Y page offsets of an element
47// GetPageOffsetRight() : get X page offset of the right side of an element
48// GetPageOffsetRight() : get Y page offset of the bottom of an element
49// GetScrollTop(): get the vertical scrolling pos of a window.
50// GetScrollLeft(): get the horizontal scrolling pos of a window
51// IsScrollAtEnd(): check if window scrollbar has reached its maximum offset
52// ScrollTo(): scroll window to a position
53// ScrollIntoView(): scroll window so that an element is in view.
54// GetWindowWidth(): get width of a window.
55// GetWindowHeight(): get height of a window
56// GetAvailScreenWidth(): get available screen width
57// GetAvailScreenHeight(): get available screen height
58// GetNiceWindowHeight(): get a nice height for a new browser window.
59// Open{External/Internal}Window(): open a separate window
60// CloseWindow(): close a window
61//
62// - DOM walking utilities
63// AnnotateTerms(): find terms in a node and decorate them with some tag
64// AnnotateText(): find terms in a text node and decorate them with some tag
65//
66// - String utilties
67// HtmlEscape(): html escapes a string
68// HtmlUnescape(): remove html-escaping.
69// QuoteEscape(): escape " quotes.
70// CollapseWhitespace(): collapse multiple whitespace into one whitespace.
71// Trim(): trim whitespace on ends of string
72// IsEmpty(): check if CollapseWhiteSpace(String) == ""
73// IsLetterOrDigit(): check if a character is a letter or a digit
74// ConvertEOLToLF(): normalize the new-lines of a string.
75// HtmlEscapeInsertWbrs(): HtmlEscapes and inserts <wbr>s (word break tags)
76// after every n non-space chars and/or after or before certain special chars
77//
78// - TextArea utilities
79// GetCursorPos(): finds the cursor position of a textfield
80// SetCursorPos(): sets the cursor position in a textfield
81//
82// - Array utilities
83// FindInArray(): do a linear search to find an element value.
84// DeleteArrayElement(): return a new array with a specific value removed.
85// CloneObject(): clone an object, copying its values recursively.
86// CloneEvent(): clone an event; cannot use CloneObject because it
87// suffers from infinite recursion
88//
89// - Formatting utilities
90// PrintArray(): used to print/generate HTML by combining static text
91// and dynamic strings.
92// ImageHtml(): create html for an img tag
93// FormatJSLink(): formats a link that invokes js code when clicked.
94// MakeId3(): formats an id that has two id numbers, eg, foo_3_7
95//
96// - Timeouts
97// SafeTimeout(): sets a timeout with protection against ugly JS-errors
98// CancelTimeout(): cancels a timeout with a given ID
99// CancelAllTimeouts(): cancels all timeouts on a given window
100//
101// - Miscellaneous
102// IsDefined(): returns true if argument is not undefined
103// ------------------------------------------------------------------------
104
105// browser detection
106function BR_AgentContains_(str) {
107 if (str in BR_AgentContains_cache_) {
108 return BR_AgentContains_cache_[str];
109 }
110
111 return BR_AgentContains_cache_[str] =
112 (navigator.userAgent.toLowerCase().indexOf(str) != -1);
113}
114// We cache the results of the indexOf operation. This gets us a 10x benefit in
115// Gecko, 8x in Safari and 4x in MSIE for all of the browser checks
116var BR_AgentContains_cache_ = {};
117
118function BR_IsIE() {
119 return (BR_AgentContains_('msie') || BR_AgentContains_('trident')) &&
120 !window.opera;
121}
122
123function BR_IsKonqueror() {
124 return BR_AgentContains_('konqueror');
125}
126
127function BR_IsSafari() {
128 return BR_AgentContains_('safari') || BR_IsKonqueror();
129}
130
131function BR_IsNav() {
132 return !BR_IsIE() &&
133 !BR_IsSafari() &&
134 BR_AgentContains_('mozilla');
135}
136
137var BACKSPACE_KEYNAME = 'Backspace';
138var COMMA_KEYNAME = ',';
139var DELETE_KEYNAME = 'Delete';
140var UP_KEYNAME = 'ArrowUp';
141var DOWN_KEYNAME = 'ArrowDown';
142var LEFT_KEYNAME = 'ArrowLeft';
143var RIGHT_KEYNAME = 'ArrowRight';
144var ENTER_KEYNAME = 'Enter';
145var ESC_KEYNAME = 'Escape';
146var SPACE_KEYNAME = ' ';
147var TAB_KEYNAME = 'Tab';
148var SHIFT_KEYNAME = 'Shift';
149var PAGE_DOWN_KEYNAME = 'PageDown';
150var PAGE_UP_KEYNAME = 'PageUp';
151
152var MAX_EMAIL_ADDRESS_LENGTH = 320; // 64 + '@' + 255
153var MAX_SIGNATURE_LENGTH = 1000; // 1000 chars of maximum signature
154
155// ------------------------------------------------------------------------
156// Assertions
157// DEPRECATED: Use assert.js
158// ------------------------------------------------------------------------
159/**
160 * DEPRECATED: Use assert.js
161 */
162function raise(msg) {
163 if (typeof Error != 'undefined') {
164 throw new Error(msg || 'Assertion Failed');
165 } else {
166 throw (msg);
167 }
168}
169
170/**
171 * DEPRECATED: Use assert.js
172 *
173 * Fail() is useful for marking logic paths that should
174 * not be reached. For example, if you have a class that uses
175 * ints for enums:
176 *
177 * MyClass.ENUM_FOO = 1;
178 * MyClass.ENUM_BAR = 2;
179 * MyClass.ENUM_BAZ = 3;
180 *
181 * And a switch statement elsewhere in your code that
182 * has cases for each of these enums, then you can
183 * "protect" your code as follows:
184 *
185 * switch(type) {
186 * case MyClass.ENUM_FOO: doFooThing(); break;
187 * case MyClass.ENUM_BAR: doBarThing(); break;
188 * case MyClass.ENUM_BAZ: doBazThing(); break;
189 * default:
190 * Fail("No enum in MyClass with value: " + type);
191 * }
192 *
193 * This way, if someone introduces a new value for this enum
194 * without noticing this switch statement, then the code will
195 * fail if the logic allows it to reach the switch with the
196 * new value, alerting the developer that they should add a
197 * case to the switch to handle the new value they have introduced.
198 *
199 * @param {string} opt_msg to display for failure
200 * DEFAULT: "Assertion failed"
201 */
202function Fail(opt_msg) {
203 opt_msg = opt_msg || 'Assertion failed';
204 if (IsDefined(DumpError)) DumpError(opt_msg + '\n');
205 raise(opt_msg);
206}
207
208/**
209 * DEPRECATED: Use assert.js
210 *
211 * Asserts that an expression is true (non-zero and non-null).
212 *
213 * Note that it is critical not to pass logic
214 * with side-effects as the expression for AssertTrue
215 * because if the assertions are removed by the
216 * JSCompiler, then the expression will be removed
217 * as well, in which case the side-effects will
218 * be lost. So instead of this:
219 *
220 * AssertTrue( criticalComputation() );
221 *
222 * Do this:
223 *
224 * var result = criticalComputation();
225 * AssertTrue(result);
226 *
227 * @param expression to evaluate
228 * @param {string} opt_msg to display if the assertion fails
229 *
230 */
231function AssertTrue(expression, opt_msg) {
232 if (!expression) {
233 opt_msg = opt_msg || 'Assertion failed';
234 Fail(opt_msg);
235 }
236}
237
238/**
239 * DEPRECATED: Use assert.js
240 *
241 * Asserts that a value is of the provided type.
242 *
243 * AssertType(6, Number);
244 * AssertType("ijk", String);
245 * AssertType([], Array);
246 * AssertType({}, Object);
247 * AssertType(ICAL_Date.now(), ICAL_Date);
248 *
249 * @param value
250 * @param type A constructor function
251 * @param {string} opt_msg to display if the assertion fails
252 */
253function AssertType(value, type, opt_msg) {
254 // for backwards compatability only
255 if (typeof value == type) return;
256
257 if (value || value == '') {
258 try {
259 if (type == AssertTypeMap[typeof value] || value instanceof type) return;
260 } catch (e) {/* failure, type was an illegal argument to instanceof */}
261 }
262 let makeMsg = opt_msg === undefined;
263 if (makeMsg) {
264 if (typeof type == 'function') {
265 let match = type.toString().match(/^\s*function\s+([^\s\{]+)/);
266 if (match) type = match[1];
267 }
268 opt_msg = 'AssertType failed: <' + value + '> not typeof '+ type;
269 }
270 Fail(opt_msg);
271}
272
273var AssertTypeMap = {
274 'string': String,
275 'number': Number,
276 'boolean': Boolean,
277};
278
279var EXPIRED_COOKIE_VALUE = 'EXPIRED';
280
281
282// ------------------------------------------------------------------------
283// Window/screen utilities
284// TODO: these should be renamed (e.g. GetWindowWidth to GetWindowInnerWidth
285// and moved to geom.js)
286// ------------------------------------------------------------------------
287// Get page offset of an element
288function GetPageOffsetLeft(el) {
289 let x = el.offsetLeft;
290 if (el.offsetParent != null) {
291 x += GetPageOffsetLeft(el.offsetParent);
292 }
293 return x;
294}
295
296// Get page offset of an element
297function GetPageOffsetTop(el) {
298 let y = el.offsetTop;
299 if (el.offsetParent != null) {
300 y += GetPageOffsetTop(el.offsetParent);
301 }
302 return y;
303}
304
305// Get page offset of an element
306function GetPageOffset(el) {
307 let x = el.offsetLeft;
308 let y = el.offsetTop;
309 if (el.offsetParent != null) {
310 let pos = GetPageOffset(el.offsetParent);
311 x += pos.x;
312 y += pos.y;
313 }
314 return {x: x, y: y};
315}
316
317// Get the y position scroll offset.
318function GetScrollTop(win) {
319 return GetWindowPropertyByBrowser_(win, getScrollTopGetters_);
320}
321
322var getScrollTopGetters_ = {
323 ieQuirks_: function(win) {
324 return win.document.body.scrollTop;
325 },
326 ieStandards_: function(win) {
327 return win.document.documentElement.scrollTop;
328 },
329 dom_: function(win) {
330 return win.pageYOffset;
331 },
332};
333
334// Get the x position scroll offset.
335function GetScrollLeft(win) {
336 return GetWindowPropertyByBrowser_(win, getScrollLeftGetters_);
337}
338
339var getScrollLeftGetters_ = {
340 ieQuirks_: function(win) {
341 return win.document.body.scrollLeft;
342 },
343 ieStandards_: function(win) {
344 return win.document.documentElement.scrollLeft;
345 },
346 dom_: function(win) {
347 return win.pageXOffset;
348 },
349};
350
351// Scroll so that as far as possible the entire element is in view.
352var ALIGN_BOTTOM = 'b';
353var ALIGN_MIDDLE = 'm';
354var ALIGN_TOP = 't';
355
356var getWindowWidthGetters_ = {
357 ieQuirks_: function(win) {
358 return win.document.body.clientWidth;
359 },
360 ieStandards_: function(win) {
361 return win.document.documentElement.clientWidth;
362 },
363 dom_: function(win) {
364 return win.innerWidth;
365 },
366};
367
368function GetWindowHeight(win) {
369 return GetWindowPropertyByBrowser_(win, getWindowHeightGetters_);
370}
371
372var getWindowHeightGetters_ = {
373 ieQuirks_: function(win) {
374 return win.document.body.clientHeight;
375 },
376 ieStandards_: function(win) {
377 return win.document.documentElement.clientHeight;
378 },
379 dom_: function(win) {
380 return win.innerHeight;
381 },
382};
383
384/**
385 * Allows the easy use of different getters for IE quirks mode, IE standards
386 * mode and fully DOM-compliant browers.
387 *
388 * @param win window to get the property for
389 * @param getters object with various getters. Invoked with the passed window.
390 * There are three properties:
391 * - ieStandards_: IE 6.0 standards mode
392 * - ieQuirks_: IE 6.0 quirks mode and IE 5.5 and older
393 * - dom_: Mozilla, Safari and other fully DOM compliant browsers
394 *
395 * @private
396 */
397function GetWindowPropertyByBrowser_(win, getters) {
398 try {
399 if (BR_IsSafari()) {
400 return getters.dom_(win);
401 } else if (!window.opera &&
402 'compatMode' in win.document &&
403 win.document.compatMode == 'CSS1Compat') {
404 return getters.ieStandards_(win);
405 } else if (BR_IsIE()) {
406 return getters.ieQuirks_(win);
407 }
408 } catch (e) {
409 // Ignore for now and fall back to DOM method
410 }
411
412 return getters.dom_(win);
413}
414
415function GetAvailScreenWidth(win) {
416 return win.screen.availWidth;
417}
418
419// Used for horizontally centering a new window of the given width in the
420// available screen. Set the new window's distance from the left of the screen
421// equal to this function's return value.
422// Params: width: the width of the new window
423// Returns: the distance from the left edge of the screen for the new window to
424// be horizontally centered
425function GetCenteringLeft(win, width) {
426 return (win.screen.availWidth - width) >> 1;
427}
428
429// Used for vertically centering a new window of the given height in the
430// available screen. Set the new window's distance from the top of the screen
431// equal to this function's return value.
432// Params: height: the height of the new window
433// Returns: the distance from the top edge of the screen for the new window to
434// be vertically aligned.
435function GetCenteringTop(win, height) {
436 return (win.screen.availHeight - height) >> 1;
437}
438
439/**
440 * Opens a child popup window that has no browser toolbar/decorations.
441 * (Copied from caribou's common.js library with small modifications.)
442 *
443 * @param url the URL for the new window (Note: this will be unique-ified)
444 * @param opt_name the name of the new window
445 * @param opt_width the width of the new window
446 * @param opt_height the height of the new window
447 * @param opt_center if true, the new window is centered in the available screen
448 * @param opt_hide_scrollbars if true, the window hides the scrollbars
449 * @param opt_noresize if true, makes window unresizable
450 * @param opt_blocked_msg message warning that the popup has been blocked
451 * @return {Window} a reference to the new child window
452 */
453function Popup(url, opt_name, opt_width, opt_height, opt_center,
454 opt_hide_scrollbars, opt_noresize, opt_blocked_msg) {
455 if (!opt_height) {
456 opt_height = Math.floor(GetWindowHeight(window.top) * 0.8);
457 }
458 if (!opt_width) {
459 opt_width = Math.min(GetAvailScreenWidth(window), opt_height);
460 }
461
462 let features = 'resizable=' + (opt_noresize ? 'no' : 'yes') + ',' +
463 'scrollbars=' + (opt_hide_scrollbars ? 'no' : 'yes') + ',' +
464 'width=' + opt_width + ',height=' + opt_height;
465 if (opt_center) {
466 features += ',left=' + GetCenteringLeft(window, opt_width) + ',' +
467 'top=' + GetCenteringTop(window, opt_height);
468 }
469 return OpenWindow(window, url, opt_name, features, opt_blocked_msg);
470}
471
472/**
473 * Opens a new window. Returns the new window handle. Tries to open the new
474 * window using top.open() first. If that doesn't work, then tries win.open().
475 * If that still doesn't work, prints an alert.
476 * (Copied from caribou's common.js library with small modifications.)
477 *
478 * @param win the parent window from which to open the new child window
479 * @param url the URL for the new window (Note: this will be unique-ified)
480 * @param opt_name the name of the new window
481 * @param opt_features the properties of the new window
482 * @param opt_blocked_msg message warning that the popup has been blocked
483 * @return {Window} a reference to the new child window
484 */
485function OpenWindow(win, url, opt_name, opt_features, opt_blocked_msg) {
486 let newwin = OpenWindowHelper(top, url, opt_name, opt_features);
487 if (!newwin || newwin.closed || !newwin.focus) {
488 newwin = OpenWindowHelper(win, url, opt_name, opt_features);
489 }
490 if (!newwin || newwin.closed || !newwin.focus) {
491 if (opt_blocked_msg) alert(opt_blocked_msg);
492 } else {
493 // Make sure that the window has the focus
494 newwin.focus();
495 }
496 return newwin;
497}
498
499/*
500 * Helper for OpenWindow().
501 * (Copied from caribou's common.js library with small modifications.)
502 */
503function OpenWindowHelper(win, url, name, features) {
504 let newwin;
505 if (features) {
506 newwin = win.open(url, name, features);
507 } else if (name) {
508 newwin = win.open(url, name);
509 } else {
510 newwin = win.open(url);
511 }
512 return newwin;
513}
514
515// ------------------------------------------------------------------------
516// String utilities
517// ------------------------------------------------------------------------
518// Do html escaping
519var amp_re_ = /&/g;
520var lt_re_ = /</g;
521var gt_re_ = />/g;
522
523// converts multiple ws chars to a single space, and strips
524// leading and trailing ws
525var spc_re_ = /\s+/g;
526var beg_spc_re_ = /^ /;
527var end_spc_re_ = / $/;
528
529var newline_re_ = /\r?\n/g;
530var spctab_re_ = /[ \t]+/g;
531var nbsp_re_ = /\xa0/g;
532
533// URL-decodes the string. We need to specially handle '+'s because
534// the javascript library doesn't properly convert them to spaces
535var plus_re_ = /\+/g;
536
537// Converts any instances of "\r" or "\r\n" style EOLs into "\n" (Line Feed),
538// and also trim the extra newlines and whitespaces at the end.
539var eol_re_ = /\r\n?/g;
540var trailingspc_re_ = /[\n\t ]+$/;
541
542// Converts a string to its canonicalized label form.
543var illegal_chars_re_ = /[ \/(){}&|\\\"\000]/g;
544
545// ------------------------------------------------------------------------
546// TextArea utilities
547// ------------------------------------------------------------------------
548
549// Gets the cursor pos in a text area. Returns -1 if the cursor pos cannot
550// be determined or if the cursor out of the textfield.
551function GetCursorPos(win, textfield) {
552 try {
553 if (IsDefined(textfield.selectionEnd)) {
554 // Mozilla directly supports this
555 return textfield.selectionEnd;
556 } else if (win.document.selection && win.document.selection.createRange) {
557 // IE doesn't export an accessor for the endpoints of a selection.
558 // Instead, it uses the TextRange object, which has an extremely obtuse
559 // API. Here's what seems to work:
560
561 // (1) Obtain a textfield from the current selection (cursor)
562 let tr = win.document.selection.createRange();
563
564 // Check if the current selection is in the textfield
565 if (tr.parentElement() != textfield) {
566 return -1;
567 }
568
569 // (2) Make a text range encompassing the textfield
570 let tr2 = tr.duplicate();
571 tr2.moveToElementText(textfield);
572
573 // (3) Move the end of the copy to the beginning of the selection
574 tr2.setEndPoint('EndToStart', tr);
575
576 // (4) The span of the textrange copy is equivalent to the cursor pos
577 let cursor = tr2.text.length;
578
579 // Finally, perform a sanity check to make sure the cursor is in the
580 // textfield. IE sometimes screws this up when the window is activated
581 if (cursor > textfield.value.length) {
582 return -1;
583 }
584 return cursor;
585 } else {
586 Debug('Unable to get cursor position for: ' + navigator.userAgent);
587
588 // Just return the size of the textfield
589 // TODO: Investigate how to get cursor pos in Safari!
590 return textfield.value.length;
591 }
592 } catch (e) {
593 DumpException(e, 'Cannot get cursor pos');
594 }
595
596 return -1;
597}
598
599function SetCursorPos(win, textfield, pos) {
600 if (IsDefined(textfield.selectionEnd) &&
601 IsDefined(textfield.selectionStart)) {
602 // Mozilla directly supports this
603 textfield.selectionStart = pos;
604 textfield.selectionEnd = pos;
605 } else if (win.document.selection && textfield.createTextRange) {
606 // IE has textranges. A textfield's textrange encompasses the
607 // entire textfield's text by default
608 let sel = textfield.createTextRange();
609
610 sel.collapse(true);
611 sel.move('character', pos);
612 sel.select();
613 }
614}
615
616// ------------------------------------------------------------------------
617// Array utilities
618// ------------------------------------------------------------------------
619// Find an item in an array, returns the key, or -1 if not found
620function FindInArray(array, x) {
621 for (let i = 0; i < array.length; i++) {
622 if (array[i] == x) {
623 return i;
624 }
625 }
626 return -1;
627}
628
629// Delete an element from an array
630function DeleteArrayElement(array, x) {
631 let i = 0;
632 while (i < array.length && array[i] != x) {
633 i++;
634 }
635 array.splice(i, 1);
636}
637
638// Clean up email address:
639// - remove extra spaces
640// - Surround name with quotes if it contains special characters
641// to check if we need " quotes
642// Note: do not use /g in the regular expression, otherwise the
643// regular expression cannot be reusable.
644var specialchars_re_ = /[()<>@,;:\\\".\[\]]/;
645
646// ------------------------------------------------------------------------
647// Timeouts
648//
649// It is easy to forget to put a try/catch block around a timeout function,
650// and the result is an ugly user visible javascript error.
651// Also, it would be nice if a timeout associated with a window is
652// automatically cancelled when the user navigates away from that window.
653//
654// When storing timeouts in a window, we can't let that variable be renamed
655// since the window could be top.js, and renaming such a property could
656// clash with any of the variables/functions defined in top.js.
657// ------------------------------------------------------------------------
658/**
659 * Sets a timeout safely.
660 * @param win the window object. If null is passed in, then a timeout if set
661 * on the js frame. If the window is closed, or freed, the timeout is
662 * automaticaaly cancelled
663 * @param fn the callback function: fn(win) will be called.
664 * @param ms number of ms the callback should be called later
665 */
666function SafeTimeout(win, fn, ms) {
667 if (!win) win = window;
668 if (!win._tm) {
669 win._tm = [];
670 }
671 let timeoutfn = SafeTimeoutFunction_(win, fn);
672 let id = win.setTimeout(timeoutfn, ms);
673
674 // Save the id so that it can be removed from the _tm array
675 timeoutfn.id = id;
676
677 // Safe the timeout in the _tm array
678 win._tm[id] = 1;
679
680 return id;
681}
682
683/** Creates a callback function for a timeout*/
684function SafeTimeoutFunction_(win, fn) {
685 var timeoutfn = function() {
686 try {
687 fn(win);
688
689 let t = win._tm;
690 if (t) {
691 delete t[timeoutfn.id];
692 }
693 } catch (e) {
694 DumpException(e);
695 }
696 };
697 return timeoutfn;
698}
699
700// ------------------------------------------------------------------------
701// Misc
702// ------------------------------------------------------------------------
703// Check if a value is defined
704function IsDefined(value) {
705 return (typeof value) != 'undefined';
706}