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