Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(188)

Side by Side Diff: ios/web/web_state/js/resources/core.js

Issue 1029983002: Upstream ios/web/ JS files (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // This file adheres to closure-compiler conventions in order to enable
6 // compilation with ADVANCED_OPTIMIZATIONS. In particular, members that are to
7 // be accessed externally should be specified in this['style'] as opposed to
8 // this.style because member identifiers are minified by default.
9 // See http://goo.gl/FwOgy
10
11 goog.provide('__crweb.core');
12
13 /**
14 * The Chrome object is populated in an anonymous object defined at
15 * initialization to prevent polluting the global namespace.
16 */
17
18 /* Beginning of anonymous object. */
19 new function() {
20 // TODO(jimblackler): use this namespace as a wrapper for all externally-
21 // visible functions, to be consistent with other JS scripts. crbug.com/380390
22 __gCrWeb['core'] = {};
23
24 // JavaScript errors are logged on the main application side. The handler is
25 // added ASAP to catch any errors in startup. Note this does not appear to
26 // work in iOS < 5.
27 window.addEventListener('error', function(event) {
28 // Sadly, event.filename and event.lineno are always 'undefined' and '0'
29 // with UIWebView.
30 invokeOnHost_({'command': 'window.error',
31 'message': event.message.toString()});
32 });
33
34 /**
35 * Margin in points around touchable elements (e.g. links for custom context
36 * menu).
37 * @type {number}
38 */
39 var touchMargin_ = 25;
40
41 __gCrWeb['innerSizeAsString'] = function() {
42 return window.innerWidth + '/' + window.innerHeight;
43 };
44
45 // Implementation of document.elementFromPoint that is working for iOS4 and
46 // iOS5 and that also goes into frames and iframes.
47 var elementFromPoint_ = function(x, y) {
48 var elementFromPointIsUsingViewPortCoordinates = function(win) {
49 if (win.pageYOffset > 0) { // Page scrolled down.
50 return (win.document.elementFromPoint(
51 0, win.pageYOffset + win.innerHeight - 1) === null);
52 }
53 if (win.pageXOffset > 0) { // Page scrolled to the right.
54 return (win.document.elementFromPoint(
55 win.pageXOffset + win.innerWidth - 1, 0) === null);
56 }
57 return false; // No scrolling, don't care.
58 };
59
60 var newCoordinate = function(x, y) {
61 var coordinates = {
62 x: x, y: y,
63 viewPortX: x - window.pageXOffset, viewPortY: y - window.pageYOffset,
64 useViewPortCoordinates: false,
65 window: window
66 };
67 return coordinates;
68 };
69
70 // Returns the coordinates of the upper left corner of |obj| in the
71 // coordinates of the window that |obj| is in.
72 var getPositionInWindow = function(obj) {
73 var coord = { x: 0, y: 0 };
74 while (obj.offsetParent) {
75 coord.x += obj.offsetLeft;
76 coord.y += obj.offsetTop;
77 obj = obj.offsetParent;
78 }
79 return coord;
80 };
81
82 var elementsFromCoordinates = function(coordinates) {
83 coordinates.useViewPortCoordinates = coordinates.useViewPortCoordinates ||
84 elementFromPointIsUsingViewPortCoordinates(coordinates.window);
85
86 var currentElement = null;
87 if (coordinates.useViewPortCoordinates) {
88 currentElement = coordinates.window.document.elementFromPoint(
89 coordinates.viewPortX, coordinates.viewPortY);
90 } else {
91 currentElement = coordinates.window.document.elementFromPoint(
92 coordinates.x, coordinates.y);
93 }
94 // We have to check for tagName, because if a selection is made by the
95 // UIWebView, the element we will get won't have one.
96 if (!currentElement || !currentElement.tagName) {
97 return null;
98 }
99 if (currentElement.tagName.toLowerCase() === 'iframe' ||
100 currentElement.tagName.toLowerCase() === 'frame') {
101 // The following condition is true if the iframe is in a different
102 // domain; no further information is accessible.
103 if (typeof(currentElement.contentWindow.document) == 'undefined') {
104 invokeOnHost_({
105 'command': 'window.error',
106 'message': 'iframe contentWindow.document is not accessible.'});
107 return currentElement;
108 }
109 var framePosition = getPositionInWindow(currentElement);
110 coordinates.viewPortX -=
111 framePosition.x - coordinates.window.pageXOffset;
112 coordinates.viewPortY -=
113 framePosition.y - coordinates.window.pageYOffset;
114 coordinates.window = currentElement.contentWindow;
115 coordinates.x -= framePosition.x + coordinates.window.pageXOffset;
116 coordinates.y -= framePosition.y + coordinates.window.pageYOffset;
117 return elementsFromCoordinates(coordinates);
118 }
119 return currentElement;
120 };
121
122 return elementsFromCoordinates(newCoordinate(x, y));
123 };
124
125 var spiralCoordinates = function(x, y) {
126 var coordinates = [];
127
128 var maxAngle = Math.PI * 2.0 * 3.0;
129 var pointCount = 30;
130 var angleStep = maxAngle / pointCount;
131 var speed = touchMargin_ / maxAngle;
132
133 for (var index = 0; index < pointCount; index++) {
134 var angle = angleStep * index;
135 var radius = angle * speed;
136
137 coordinates.push({x: x + Math.round(Math.cos(angle) * radius),
138 y: y + Math.round(Math.sin(angle) * radius)});
139 }
140
141 return coordinates;
142 };
143
144 // Returns the url of the image or link under the selected point. Returns an
145 // empty string if no links or images are found.
146 __gCrWeb['getElementFromPoint'] = function(x, y) {
147 var hitCoordinates = spiralCoordinates(x, y);
148 for (var index = 0; index < hitCoordinates.length; index++) {
149 var coordinates = hitCoordinates[index];
150
151 var element = elementFromPoint_(coordinates.x, coordinates.y);
152 if (!element || !element.tagName) {
153 // Nothing under the hit point. Try the next hit point.
154 continue;
155 }
156
157 if (getComputedWebkitTouchCallout_(element) === 'none')
158 continue;
159 // Also check element's ancestors. A bound on the level is used here to
160 // avoid large overhead when no links or images are found.
161 var level = 0;
162 while (++level < 8 && element && element != document) {
163 var tagName = element.tagName;
164 if (!tagName)
165 continue;
166 tagName = tagName.toLowerCase();
167
168 if (tagName === 'input' || tagName === 'textarea' ||
169 tagName === 'select' || tagName === 'option') {
170 // If the element is a known input element, stop the spiral search and
171 // return empty results.
172 return '{}';
173 }
174
175 if (tagName === 'a' && element.href) {
176 // Found a link.
177 return __gCrWeb.common.JSONStringify(
178 {href: element.href,
179 referrerPolicy: getReferrerPolicy_(element)});
180 }
181
182 if (tagName === 'img' && element.src) {
183 // Found an image.
184 var result = {src: element.src,
185 referrerPolicy: getReferrerPolicy_()};
186 // Copy the title, if any.
187 if (element.title) {
188 result.title = element.title;
189 }
190 // Check if the image is also a link.
191 var parent = element.parentNode;
192 while (parent) {
193 if (parent.tagName &&
194 parent.tagName.toLowerCase() === 'a' &&
195 parent.href) {
196 // This regex identifies strings like void(0),
197 // void(0) ;void(0);, ;;;;
198 // which result in a NOP when executed as JavaScript.
199 var regex = RegExp("^javascript:(?:(?:void\\(0\\)|;)\\s*)+$");
200 if (parent.href.match(regex)) {
201 parent = parent.parentNode;
202 continue;
203 }
204 result.href = parent.href;
205 result.referrerPolicy = getReferrerPolicy_(parent);
206 break;
207 }
208 parent = parent.parentNode;
209 }
210 return __gCrWeb.common.JSONStringify(result);
211 }
212 element = element.parentNode;
213 }
214 }
215 return '{}';
216 };
217
218 // Returns true if the top window or any frames inside contain an input
219 // field of type 'password'.
220 __gCrWeb['hasPasswordField'] = function() {
221 return hasPasswordField_(window);
222 };
223
224 // Returns a string that is formatted according to the JSON syntax rules.
225 // This is equivalent to the built-in JSON.stringify() function, but is
226 // less likely to be overridden by the website itself. This public function
227 // should not be used if spoofing it would create a security vulnerability.
228 // The |__gCrWeb| object itself does not use it; it uses its private
229 // counterpart instead.
230 // Prevents websites from changing stringify's behavior by adding the
231 // method toJSON() by temporarily removing it.
232 __gCrWeb['stringify'] = function(value) {
233 if (value === null)
234 return 'null';
235 if (value === undefined)
236 return undefined;
237 if (typeof(value.toJSON) == 'function') {
238 var originalToJSON = value.toJSON;
239 value.toJSON = undefined;
240 var stringifiedValue = __gCrWeb.common.JSONStringify(value);
241 value.toJSON = originalToJSON;
242 return stringifiedValue;
243 }
244 return __gCrWeb.common.JSONStringify(value);
245 };
246
247 /*
248 * Adds the listeners that are used to handle forms, enabling autofill and
249 * the replacement method to dismiss the keyboard needed because of the
250 * Autofill keyboard accessory.
251 */
252 function addFormEventListeners_() {
253 // Focus and input events for form elements are messaged to the main
254 // application for broadcast to CRWWebControllerObservers.
255 // This is done with a single event handler for each type being added to the
256 // main document element which checks the source element of the event; this
257 // is much easier to manage than adding handlers to individual elements.
258 var formActivity = function(evt) {
259 var srcElement = evt.srcElement;
260 var fieldName = srcElement.name || '';
261 var value = srcElement.value || '';
262
263 var msg = {
264 'command': 'form.activity',
265 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form),
266 'fieldName': fieldName,
267 'type': evt.type,
268 'value': value
269 };
270 if (evt.keyCode)
271 msg.keyCode = evt.keyCode;
272 invokeOnHost_(msg);
273 };
274
275 // Focus events performed on the 'capture' phase otherwise they are often
276 // not received.
277 document.addEventListener('focus', formActivity, true);
278 document.addEventListener('blur', formActivity, true);
279 document.addEventListener('change', formActivity, true);
280
281 // Text input is watched at the bubbling phase as this seems adequate in
282 // practice and it is less obtrusive to page scripts than capture phase.
283 document.addEventListener('input', formActivity, false);
284 document.addEventListener('keyup', formActivity, false);
285 };
286
287 // Returns true if the supplied window or any frames inside contain an input
288 // field of type 'password'.
289 // @private
290 var hasPasswordField_ = function(win) {
291 var doc = win.document;
292
293 // We may will not be allowed to read the 'document' property from a frame
294 // that is in a different domain.
295 if (!doc) {
296 return false;
297 }
298
299 if (doc.querySelector('input[type=password]')) {
300 return true;
301 }
302
303 var frames = win.frames;
304 for (var i = 0; i < frames.length; i++) {
305 if (hasPasswordField_(frames[i])) {
306 return true;
307 }
308 }
309
310 return false;
311 };
312
313 function invokeOnHost_(command) {
314 __gCrWeb.message.invokeOnHost(command);
315 };
316
317 function invokeOnHostImmediate_(command) {
318 __gCrWeb.message.invokeOnHostImmediate(command);
319 };
320
321 /**
322 * Gets the referrer policy to use for navigations away from the current page.
323 * If a link element is passed, and it includes a rel=noreferrer tag, that
324 * will override the page setting.
325 * @param {HTMLElement} linkElement The link triggering the navigation.
326 * @return {String} The policy string.
327 * @private
328 */
329 var getReferrerPolicy_ = function(linkElement) {
330 if (linkElement) {
331 var rel = linkElement.getAttribute('rel');
332 if (rel && rel.toLowerCase() == 'noreferrer') {
333 return 'never';
334 }
335 }
336
337 var metaTags = document.getElementsByTagName('meta');
338 for (var i = 0; i < metaTags.length; ++i) {
339 if (metaTags[i].name.toLowerCase() == 'referrer') {
340 return metaTags[i].content.toLowerCase();
341 }
342 }
343 return 'default';
344 };
345
346 // Provides a way for other injected javascript to access the page's referrer
347 // policy.
348 __gCrWeb['getPageReferrerPolicy'] = function() {
349 return getReferrerPolicy_();
350 };
351
352 // Various aspects of global DOM behavior are overridden here.
353
354 // A popstate event needs to be fired anytime the active history entry
355 // changes. Either via back, forward, go navigation or by loading the URL,
356 // clicking on a link, etc.
357 __gCrWeb['dispatchPopstateEvent'] = function(stateObject) {
358 var popstateEvent = window.document.createEvent('HTMLEvents');
359 popstateEvent.initEvent('popstate', true, false);
360 if (stateObject)
361 popstateEvent.state = JSON.parse(stateObject);
362
363 // setTimeout() is used in order to return immediately. Otherwise the
364 // dispatchEvent call waits for all event handlers to return, which could
365 // cause a ReentryGuard failure.
366 window.setTimeout(function() {
367 window.dispatchEvent(popstateEvent);
368 }, 0);
369 };
370
371 // Keep the original replaceState() method. It's needed to update UIWebView's
372 // URL and window.history.state property during history navigations that don't
373 // cause a page load.
374 var originalWindowHistoryReplaceState = window.history.replaceState;
375 __gCrWeb['replaceWebViewURL'] = function(url, stateObject) {
376 originalWindowHistoryReplaceState.call(history, stateObject, null, url);
377 };
378
379 // Intercept window.history methods to call back/forward natively.
380 window.history.back = function() {
381 invokeOnHost_({'command': 'window.history.back'});
382 };
383 window.history.forward = function() {
384 invokeOnHost_({'command': 'window.history.forward'});
385 };
386 window.history.go = function(delta) {
387 invokeOnHost_({'command': 'window.history.go', 'value': delta});
388 };
389 window.history.pushState = function(stateObject, pageTitle, pageUrl) {
390 __gCrWeb.core_dynamic.historyWillChangeState();
391 // Calling stringify() on undefined causes a JSON parse error.
392 var serializedState =
393 typeof(stateObject) == 'undefined' ? '' :
394 __gCrWeb.common.JSONStringify(stateObject);
395 pageUrl = pageUrl || window.location.href;
396 originalWindowHistoryReplaceState.call(history, stateObject, null, pageUrl);
397 invokeOnHost_({'command': 'window.history.didPushState',
398 'stateObject': serializedState,
399 'baseUrl': document.baseURI,
400 'pageUrl': pageUrl.toString()});
401 };
402 window.history.replaceState = function(stateObject, pageTitle, pageUrl) {
403 __gCrWeb.core_dynamic.historyWillChangeState();
404 // Calling stringify() on undefined causes a JSON parse error.
405 var serializedState =
406 typeof(stateObject) == 'undefined' ? '' :
407 __gCrWeb.common.JSONStringify(stateObject);
408 pageUrl = pageUrl || window.location.href;
409 originalWindowHistoryReplaceState.call(history, stateObject, null, pageUrl);
410 invokeOnHost_({'command': 'window.history.didReplaceState',
411 'stateObject': serializedState,
412 'baseUrl': document.baseURI,
413 'pageUrl': pageUrl.toString()});
414 };
415
416 __gCrWeb['getFullyQualifiedURL'] = function(originalURL) {
417 // A dummy anchor (never added to the document) is used to obtain the
418 // fully-qualified URL of |originalURL|.
419 var anchor = document.createElement('a');
420 anchor.href = originalURL;
421 return anchor.href;
422 };
423
424 // Intercept window.close calls.
425 window.close = function() {
426 invokeOnHost_({'command': 'window.close.self'});
427 };
428
429 window.addEventListener('hashchange', function(evt) {
430 invokeOnHost_({'command': 'window.hashchange'});
431 });
432
433 __gCrWeb.core_dynamic.addEventListeners();
434
435 // Returns if a frame with |name| is found in |currentWindow|.
436 // Note frame.name is undefined for cross domain frames.
437 var hasFrame_ = function(currentWindow, name) {
438 if (currentWindow.name === name)
439 return true;
440
441 var frames = currentWindow.frames;
442 for (var index = 0; index < frames.length; ++index) {
443 var frame = frames[index];
444 if (frame === undefined)
445 continue;
446 if (hasFrame_(frame, name))
447 return true;
448 }
449 return false;
450 };
451
452 // Checks if |node| is an anchor to be opened in the current tab.
453 var isInternaLink_ = function(node) {
454 if (!node instanceof HTMLAnchorElement)
455 return false;
456
457 // Anchor with href='javascript://.....' will be opened in the current tab
458 // for simplicity.
459 if (node.href.indexOf('javascript:') == 0)
460 return true;
461
462 // UIWebView will take care of the following cases.
463 //
464 // - If the given browsing context name is the empty string or '_self', then
465 // the chosen browsing context must be the current one.
466 //
467 // - If the given browsing context name is '_parent', then the chosen
468 // browsing context must be the parent browsing context of the current
469 // one, unless there is no one, in which case the chosen browsing context
470 // must be the current browsing context.
471 //
472 // - If the given browsing context name is '_top', then the chosen browsing
473 // context must be the top-level browsing context of the current one, if
474 // there is one, or else the current browsing context.
475 //
476 // Here an undefined target is considered in the same way as an empty
477 // target.
478 if (node.target === undefined || node.target === '' ||
479 node.target === '_self' || node.target === '_parent' ||
480 node.target === '_top') {
481 return true;
482 }
483
484 // A new browsing context is being requested for an '_blank' target.
485 if (node.target === '_blank')
486 return false;
487
488 // Otherwise UIWebView will take care of the case where there exists a
489 // browsing context whose name is the same as the given browsing context
490 // name. If there is no such a browsing context, a new browsing context is
491 // being requested.
492 return hasFrame_(window, node.target);
493 };
494
495 var getTargetLink_ = function(target) {
496 var node = target;
497 // Find the closest ancester that is a link.
498 while (node) {
499 if (node instanceof HTMLAnchorElement)
500 break;
501 node = node.parentNode;
502 }
503 return node;
504 };
505
506 var setExternalRequest_ = function(href, target) {
507 if (typeof(target) == 'undefined' || target == '_blank' || target == '') {
508 target = '' + Date.now() + '-' + Math.random();
509 }
510 if (typeof(href) == 'undefined') {
511 // W3C recommended behavior.
512 href = 'about:blank';
513 }
514 // ExternalRequest messages need to be handled before the expected
515 // shouldStartLoadWithRequest, as such we cannot wait for the regular
516 // message queue invoke which delays to avoid illegal recursion into
517 // UIWebView. This immediate class of messages is handled ASAP by
518 // CRWWebController.
519 invokeOnHostImmediate_({'command': 'externalRequest',
520 'href': href,
521 'target': target,
522 'referrerPolicy': getReferrerPolicy_()});
523 };
524
525 var resetExternalRequest_ = function() {
526 invokeOnHost_({'command': 'resetExternalRequest'});
527 };
528
529 var clickBubbleListener_ = function(evt) {
530 if (evt['defaultPrevented']) {
531 resetExternalRequest_();
532 }
533 // Remove the listener.
534 evt.currentTarget.removeEventListener(
535 'click', clickBubbleListener_, false);
536 };
537
538 var getComputedWebkitTouchCallout_ = function(element) {
539 return window.getComputedStyle(element, null)['webkitTouchCallout'];
540 };
541
542 /**
543 * This method applies the various document-level overrides. Sometimes the
544 * document object gets reset in the early stages of the page lifecycle, so
545 * this is exposed as a method for the application to invoke later. That way
546 * the window-level overrides can be applied as soon as possible.
547 */
548 __gCrWeb.core.documentInject = function() {
549 // Perform web view specific operations requiring document.body presence.
550 // If necessary returns and waits for document to be present.
551 if (!__gCrWeb.core_dynamic.documentInject())
552 return;
553
554 document.addEventListener('click', function(evt) {
555 var node = getTargetLink_(evt.target);
556
557 if (!node)
558 return;
559
560 if (isInternaLink_(node)) {
561 return;
562 }
563 setExternalRequest_(node.href, node.target);
564 // Add listener to the target and its immediate ancesters. These event
565 // listeners will be removed if they get called. The listeners for some
566 // elements might never be removed, but if multiple identical event
567 // listeners are registered on the same event target with the same
568 // parameters the duplicate instances are discarded.
569 for (var level = 0; level < 5; ++level) {
570 if (node && node != document) {
571 node.addEventListener('click', clickBubbleListener_, false);
572 node = node.parentNode;
573 } else {
574 break;
575 }
576 }
577 }, true);
578
579 // Intercept clicks on anchors (links) during bubbling phase so that the
580 // browser can handle target type appropriately.
581 document.addEventListener('click', function(evt) {
582 var node = getTargetLink_(evt.target);
583
584 if (!node)
585 return;
586
587 if (isInternaLink_(node)) {
588 if (evt['defaultPrevented'])
589 return;
590 // Internal link. The web view will handle navigation, but register
591 // the anchor for UIWebView to start the progress indicator ASAP and
592 // notify web controller as soon as possible of impending navigation.
593 if (__gCrWeb.core_dynamic.handleInternalClickEvent) {
594 __gCrWeb.core_dynamic.handleInternalClickEvent(node);
595 }
596 return;
597 } else {
598 // Resets the external request if it has been canceled, otherwise
599 // updates the href in case it has been changed.
600 if (evt['defaultPrevented'])
601 resetExternalRequest_();
602 else
603 setExternalRequest_(node.href, node.target);
604 }
605 }, false);
606
607 // Capture form submit actions.
608 document.addEventListener('submit', function(evt) {
609 if (evt['defaultPrevented'])
610 return;
611
612 var form = evt.target;
613 var targetsFrame = form.target && hasFrame_(window, form.target);
614 // TODO(stuartmorgan): Handle external targets. crbug.com/233543
615
616 var action = form.getAttribute('action');
617 // Default action is to re-submit to same page.
618 if (!action)
619 action = document.location.href;
620 invokeOnHost_({
621 'command': 'document.submit',
622 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement),
623 'href': __gCrWeb['getFullyQualifiedURL'](action),
624 'targetsFrame': targetsFrame
625 });
626 }, false);
627
628 addFormEventListeners_();
629
630 // Handle or wait for and handle document load completion, if applicable.
631 if (__gCrWeb.core_dynamic.handleDocumentLoaded)
632 __gCrWeb.core_dynamic.handleDocumentLoaded();
633
634 return true;
635 };
636
637 __gCrWeb.core.documentInject();
638
639 // Form prototype loaded with event to supply Autocomplete API
640 // functionality.
641 HTMLFormElement.prototype.requestAutocomplete = function() {
642 invokeOnHost_(
643 {'command': 'form.requestAutocomplete',
644 'formName': __gCrWeb.common.getFormIdentifier(this)});
645 };
646 } // End of anonymous object
OLDNEW
« no previous file with comments | « ios/web/web_state/js/resources/console.js ('k') | ios/web/web_state/js/resources/core_dynamic_ui.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698