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

Side by Side Diff: ios/chrome/browser/ui/contextual_search/resources/contextualsearch.js

Issue 2553563004: [ios] Adds JS resources for various features. (Closed)
Patch Set: Upload. Created 4 years 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 2014 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 /**
6 * @fileoverview Support code for the Contextual Search feature. Given a tap
7 * location, locates and highlights the word at that location, and retuns
8 * some contextual data about the selection.
9 *
10 */
11
12
13 /**
14 * Namespace for this file. Depends on __gCrWeb having already been injected.
15 */
16 __gCrWeb['contextualSearch'] = {};
17
18 /* Anyonymizing block */
19 new function() {
20
21 /**
22 * Utility loggging function.
23 * @param {string} message Text of log message to be sent to application.
24 */
25 __gCrWeb['contextualSearch'].cxlog = function(message) {
26 if (Context.debugMode) {
27 console.log('[CS] ' + message);
28 }
29 };
30
31 /**
32 * Enables or disabled the selection change notification forward to the
33 * controller.
34 * @param {boolean} enabled whether to turn on (true) or off (false).
35 */
36 __gCrWeb['contextualSearch'].enableSelectionChangeListener = function(enabled) {
37 if (enabled) {
38 document.addEventListener('selectionchange',
39 Context.selectionChanged,
40 true);
41 } else {
42 document.removeEventListener('selectionchange',
43 Context.selectionChanged,
44 true);
45 }
46 };
47
48 __gCrWeb['contextualSearch'].getMutatedElementCount = function() {
49 return Context.mutationCount;
50 };
51
52 /**
53 * Enables the DOM mutation event listener.
54 * @param {number} delay after a mutation event before a tap event can be
55 * handled.
56 */
57 __gCrWeb['contextualSearch'].setMutationObserverDelay = function(delay) {
58 Context.DOMMutationTimeoutMillisecond = delay;
59
60 // select the target node
61 var target = document.body;
62
63 // create an observer instance
64 Context.DOMMutationObserver = new MutationObserver(function(mutations) {
65 // Clear any mutation records older than |DOMMutationTimeoutMillisecond|.
66 // Only do this every |DOMMutationTimeoutMillisecond|s to avoid thrashing
67 // on pages with (for example) continuous animations.
68
69 var d = new Date();
70 if ((d.getTime() - Context.lastMutationPrune) >
71 Context.DOMMutationTimeoutMillisecond) {
72 Context.processMutations(function(mutationId, mutationTime) {
73 if ((d.getTime() - mutationTime) <=
74 Context.DOMMutationTimeoutMillisecond) {
75 Context.clearMutation(mutationId);
76 }
77 return false;
78 });
79 Context.lastMutationPrune = d.getTime();
80 }
81
82 mutations.forEach(function(mutation) {
83 // Don't count mutations with invalid targets.
84 if (!mutation.target) {
85 return;
86 }
87 // Don't count mutations to invisible elements.
88 if (mutation.type != 'characterData' && !mutation.target.offsetParent) {
89 return;
90 }
91 // Don't count attribute mutations where the attribute's current and
92 // old values are the same. Also don't count attribute mutation where
93 // the current and old values are both "false-ish" (so changing a null to
94 // an empty string has no effect).
95 if (mutation.type == 'attributes') {
96 if (mutation.target.getAttribute(mutation.attributeName) ==
97 mutation.oldValue) {
98 return;
99 }
100 if (!mutation.target.getAttribute(mutation.attributeName) &&
101 !mutation.oldValue) {
102 return;
103 }
104 // Don't count the attribute mutation from setting an ID for tracking or
105 // if it is unsetting the ID.
106 if (mutation.attributeName == 'id' &&
107 (mutation.target.id.match(/__CTXSM___\d__\d+/) ||
108 mutation.target.id == '')) {
109 return;
110 }
111 }
112
113 if (mutation.type == 'characterData' &&
114 mutation.target.textContent == mutation.oldValue) {
115 return;
116 }
117
118 // If this is the first mutation after tap, and the mutation target
119 // intersects with the highlighted elements, forward it to the Chrome
120 // application to check if CS must be dismissed.
121 if (Context.highlightRange &&
122 !Context.mutationEventForwarded &&
123 Context.highlightRange.intersectsNode(mutation.target)) {
124 Context.mutationEventForwarded = true;
125 __gCrWeb.message.invokeOnHost(
126 {'command' : 'contextualSearch.mutationEvent'});
127 }
128
129 Context.lastDOMMutationMillisecond = d.getTime();
130 // If the mutated item isn't an element, find its parent.
131 // If the element doesn't have an ID, assign one to it.
132 var idContainer = mutation.target;
133 if (mutation.type == 'characterData') {
134 idContainer = idContainer.parentElement;
135 }
136 var id = idContainer.id;
137 if (!id) {
138 id = Context.newID(Context.lastDOMMutationMillisecond);
139 idContainer.id = id;
140 }
141 Context.recordMutation(id, Context.lastDOMMutationMillisecond);
142 });
143 });
144
145 // configuration of the observer:
146 var config = {
147 attributes: true,
148 characterData: true,
149 subtree: true,
150 attributeOldValue: true,
151 characterDataOldValue: true
152 };
153
154 // pass in the target node, as well as the observer options
155 Context.DOMMutationObserver.observe(target, config);
156 };
157
158 /**
159 * Enables the body touch end event listener. This will catch touch events that
160 * don't call preventDefault.
161 * @param {number} delay after a mutation event before a tap event can be
162 * handled.
163 */
164 __gCrWeb['contextualSearch'].setBodyTouchListenerDelay = function(delay) {
165 Context.touchEventTimeoutMillisecond = delay;
166 Context.bodyTouchEndEventListener = function(event) {
167 if (!event.defaultPrevented) {
168 var d = new Date();
169 Context.lastTouchEventMillisecond = d.getTime();
170 } else {
171 __gCrWeb['contextualSearch'].cxlog('Touch default prevented');
172 }
173 };
174 document.body.addEventListener('touchend', Context.bodyTouchEndEventListener,
175 false);
176 };
177
178 /**
179 * Disables the DOM mutation listener.
180 */
181 __gCrWeb['contextualSearch'].disableMutationObserver = function() {
182 Context.DOMMutationObserver.disconnect();
183 Context.DOMMutationObserver = null;
184 Context.lastDOMMutationMillisecond = 0;
185 };
186
187 /**
188 * Disables the body touchend listener.
189 */
190 __gCrWeb['contextualSearch'].disableBodyTouchListener = function() {
191 document.body.removeEventListener('touchend',
192 Context.bodyTouchEndEventListener, false);
193 Context.lastTouchEventMillisecond = 0;
194 };
195
196 /**
197 * Expands the highlight to [startOffset, endOffset] in the surrounding range.
198 * @param {number} startOffset first character to include in the range.
199 * @param {number} endOffset last character to include in the range.
200 * @return {JSON} new highlighted rects.
201 */
202 __gCrWeb['contextualSearch'].expandHighlight =
203 function(startOffset, endOffset) {
204 if ((startOffset == Context.surroundingRange.highlightStartOffset &&
205 endOffset == Context.surroundingRange.highlightEndOffset) ||
206 startOffset > endOffset) {
207 return;
208 }
209 var range = Context.createSurroundingRange(startOffset, endOffset);
210 Context.highlightRange = range;
211 return __gCrWeb['contextualSearch'].highlightRects();
212 };
213
214 /**
215 * Returns the rects to draw the current highlight.
216 * @return {JSON} hilighted rects.
217 */
218 __gCrWeb['contextualSearch'].highlightRects = function() {
219 return __gCrWeb.stringify(
220 {'rects' : Context.getHighlightRects(),
221 'size': {'width' : document.documentElement.scrollWidth,
222 'height' : document.documentElement.scrollHeight
223 }});
224 };
225
226 /**
227 * Clears the current highlight.
228 */
229 __gCrWeb['contextualSearch'].clearHighlight = function() {
230 Context.highlightRange = null;
231 };
232
233 /**
234 * Retrieve the currently highlighted string.
235 * This is used for test purposes only.
236 * @return {string} the currently highlighted string.
237 */
238 __gCrWeb['contextualSearch'].retrieveHighlighted = function() {
239 return Context.rangeToString(Context.highlightRange);
240 };
241
242 /**
243 * Prepares Contextual Search for a given point in the window. This method
244 * will find which word is located at the given point, extract the context
245 * data for that word, and (if a word was located), pass the context data
246 * back to the calling application.
247 * @param {number} x The point's x coordinate as a ratio of page width.
248 * @param {number} y The point's y coordinate as a ratio of page height.
249 * @return {object} Empty if no word was found, or the context, x and y if
250 * a word was found.
251 */
252 __gCrWeb['contextualSearch'].handleTapAtPoint = function(x, y) {
253 var tapResults;
254 if (window.getSelection().toString()) {
255 tapResults = new ContextData();
256 tapResults.error = 'Failed: selection is not empty.';
257 return __gCrWeb.stringify({'context' : tapResults.returnContext()});
258 }
259 var d = new Date();
260 var lastTouchDelta = d.getTime() - Context.lastTouchEventMillisecond;
261 if (Context.touchEventTimeoutMillisecond &&
262 lastTouchDelta > Context.touchEventTimeoutMillisecond) {
263 tapResults = new ContextData();
264 tapResults.error = 'Failed: last touch was ' + lastTouchDelta +
265 'ms ago (>' + Context.touchEventTimeoutMillisecond + 'ms timeout)';
266 } else {
267 var absoluteX =
268 x * document.documentElement.scrollWidth - document.body.scrollLeft;
269 var absoluteY =
270 y * document.documentElement.scrollHeight - document.body.scrollTop;
271 tapResults = Context.getContextDataFromPoint(absoluteX, absoluteY);
272
273 if (!tapResults.error) {
274 var range = tapResults.range;
275 if (!range) {
276 tapResults.error = 'Failed: context data range was empty';
277 } else if (tapResults.surroundingText.length < tapResults.offsetEnd) {
278 tapResults.error =
279 'Failed: surrounding text is shorter than text offset';
280 } else if (tapResults.getSelectedText() != tapResults.selectedText) {
281 tapResults.error = 'Failed: offsets do not match selected text: (' +
282 tapResults.getSelectedText() + ') vs. (' +
283 tapResults.selectedText + ')';
284 }
285 }
286 }
287 Context.mutationEventForwarded = false;
288 return __gCrWeb.stringify({'context' : tapResults.returnContext()});
289 };
290
291 //------------------------------------------------------------------------------
292 // ContextData
293 //------------------------------------------------------------------------------
294
295 /**
296 * @constructor
297 */
298 var ContextData = function() {};
299
300 /**
301 * An error message, if any, associated with the context.
302 * @type {?string}
303 */
304 ContextData.prototype.error = null;
305
306 /**
307 * The range containing the selected text.
308 * @type {?Range}
309 */
310 ContextData.prototype.range = null;
311
312 /**
313 * The URL from where the context was extracted.
314 * @type {?string}
315 */
316 ContextData.prototype.url = null;
317
318 /**
319 * The selected text.
320 * @type {?string}
321 */
322 ContextData.prototype.selectedText = null;
323
324 /**
325 * The surrounding text.
326 * @type {?string}
327 */
328 ContextData.prototype.surroundingText = null;
329
330 /**
331 * The start position of the selected text relative to the surrounding text.
332 * @type {?number}
333 */
334 ContextData.prototype.offsetStart = null;
335
336 /**
337 * The end position of the selected text relative to the surrounding text.
338 * @type {?number}
339 */
340 ContextData.prototype.offsetEnd = null;
341
342 /**
343 * The rewritten query.
344 * @type {?string}
345 */
346 ContextData.prototype.rewrittenQuery = null;
347
348 /**
349 * Gets the search query for the context.
350 * @return {?string} The search query.
351 */
352 ContextData.prototype.getQuery = function() {
353 return this.rewrittenQuery || this.selectedText;
354 };
355
356 /**
357 * @return {string} The part of the surrounding text before the selected text.
358 */
359 ContextData.prototype.getTextBefore = function() {
360 var selectedText = this.selectedText;
361 var surroundingText = this.surroundingText;
362 var result = '';
363 if (!this.rewrittenQuery && surroundingText) {
364 result = surroundingText.substring(0, this.offsetStart);
365 }
366 return result;
367 };
368
369 /**
370 * @return {string} The part of the surrounding text after the selected text.
371 */
372 ContextData.prototype.getTextAfter = function() {
373 var selectedText = this.selectedText;
374 var surroundingText = this.surroundingText;
375 var result = '';
376 if (!this.rewrittenQuery && selectedText && surroundingText) {
377 result = surroundingText.substring(this.offsetEnd);
378 }
379 return result;
380 };
381
382 /**
383 * @return {string} The selected text as indicated by offsetStart and
384 * offsetEnd. This should be the same as selectedText
385 */
386 ContextData.prototype.getSelectedText = function() {
387 var surroundingText = this.surroundingText;
388 var result = '';
389 if (!this.rewrittenQuery && surroundingText) {
390 result = surroundingText.substring(this.offsetStart, this.offsetEnd);
391 }
392 return result;
393 };
394
395 /**
396 * @return {JSONDictionary} Context data assembeld for return to native app.
397 */
398 ContextData.prototype.returnContext = function() {
399 var context = {'url' : this.url,
400 'selectedText' : this.selectedText,
401 'surroundingText' : this.surroundingText,
402 'offsetStart' : this.offsetStart,
403 'offsetEnd' : this.offsetEnd,
404 'rects': Context.getHighlightRects()
405 };
406 if (this.error) {
407 context['error'] = this.error;
408 }
409 return context;
410 };
411
412 //------------------------------------------------------------------------------
413 // Context
414 //------------------------------------------------------------------------------
415
416 var Context = {};
417
418 /**
419 * Whether to send log output to the host.
420 * @type {bool}
421 */
422 Context.debugMode = false;
423
424 /**
425 * The maximium amount of time that should be spent searching for a text range,
426 * in milliseconds. If the search does not finish within the specified value,
427 * it should terminate without returning a result.
428 * @const {number}
429 */
430 Context.GET_RANGE_TIMEOUT_MS = 50;
431
432 /**
433 * Number of surrouding sentences when calculating the surrounding text.
434 * @const {number}
435 */
436 Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES = 1500;
437
438 /**
439 * Maximum number of chars for a selection to trigger the search.
440 * @const {number}
441 */
442 Context.MAX_NUMBER_OF_CHARS_IN_SELECTION = 100;
443
444 /**
445 * Last range returned by Context.extractSurroundingDataFromRange.
446 * @type {JSONObject} Contains startContainer, endContainer, startOffset,
447 * endOffset of the surrounding range and relative position of the tapped word.
448 */
449 Context.surroundingRange = null;
450
451 /**
452 * The range that is curently highlighted.
453 * @type {Range}
454 */
455 Context.highlightRange = null;
456
457 /**
458 * A boolean to check if a mutation event has been forwarded after the latest
459 * tap.
460 * @type {boolean}
461 */
462 Context.mutationEventForwarded = false;
463
464 /**
465 * A Regular Expression that matches Unicode word characters.
466 * @type {RegExp}
467 */
468 Context.reWordCharacter_ =
469 /[\u00C0-\u1FFF\u2C00-\uD7FF\w]/;
470
471 /**
472 * An observer of the DOM mutation.
473 * @type {MutationObserver}
474 */
475 Context.DOMMutationObserver = null;
476
477 /**
478 * The date of the last DOM mutation (in ms).
479 * @type {number}
480 */
481 Context.lastDOMMutationMillisecond = 0;
482
483 /**
484 * A hash of timestamps keyed by element-id.
485 * @type {Object}
486 */
487 Context.mutatedElements = {};
488
489 /**
490 * A running count of tracked mutated objects.
491 * @type {number}
492 */
493 Context.mutationCount = 0;
494
495 /**
496 * Date of the last time the mutation list was pruned of old entries (in ms).
497 * @type {number}
498 */
499 Context.lastMutationPrune = 0;
500
501 /**
502 * An incrementing integer for generating temporary element ids when needed.
503 * @type {number}
504 */
505 Context.mutationIdCounter = 0;
506
507 /**
508 * A snapshot of the previous text selection (if any), used to determine if a
509 * selection change is a new selection or not. previousSelection stores the
510 * anchor and focus nodes and offsets of the previously-reported selection.
511 * @type {Object}
512 */
513 Context.previousSelection = null;
514
515 /**
516 * Generates a string suitable for use as a temporary element id.
517 * @param {string} nonce A string that varies based on the current time.
518 * @return {string} A string to be used as an element id.
519 */
520 Context.newID = function(nonce) {
521 return '__CTXSM___' + (Context.mutationIdCounter++) + '__' + nonce;
522 };
523
524 /**
525 * The timeout of DOM mutation after which a contextual search can be triggered
526 * (in ms)
527 * @type {number}
528 */
529 Context.DOMMutationTimeoutMillisecond = 200;
530
531 /**
532 * An observer of body to catch unhandled touch events.
533 * @type {EventListener}
534 */
535 Context.bodyTouchEndEventListener = null;
536
537 /**
538 * The date of the last unhandled touch event (in ms).
539 * @type {number}
540 */
541 Context.lastTouchEventMillisecond = 0;
542
543 /**
544 * The timeout of DOM mutation after which a contextual search can be triggered
545 * (in ms)
546 * @type {number}
547 */
548 Context.touchEventTimeoutMillisecond = 0;
549
550 /**
551 * List of node types whose contents should not be parsed by Contextual Search.
552 * @type {Array.<string>}
553 */
554 Context['invalidElements_'] = [
555 'A',
556 'APPLET',
557 'AREA',
558 'AUDIO',
559 'BUTTON',
560 'CANVAS',
561 'EMBED',
562 'FRAME',
563 'FRAMESET',
564 'IFRAME',
565 'IMG',
566 'INPUT',
567 'KEYGEN',
568 'LABEL',
569 'MAP',
570 'OBJECT',
571 'OPTGROUP',
572 'OPTION',
573 'PROGRESS',
574 'SCRIPT',
575 'SELECT',
576 'TEXTAREA',
577 'VIDEO'
578 ];
579
580 /**
581 * List of ARIA roles that define widgets.
582 * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles
583 * @type {Array.<string>}
584 */
585 Context['widgetRoles_'] = [
586 'alert',
587 'alertdialog',
588 'button',
589 'checkbox',
590 'dialog',
591 'gridcell',
592 'link',
593 'log',
594 'marquee',
595 'menuitem',
596 'menuitemcheckbox',
597 'menuitemradio',
598 'option',
599 'progressbar',
600 'radio',
601 'scrollbar',
602 'slider',
603 'spinbutton',
604 'status',
605 'tab',
606 'tabpanel',
607 'textbox',
608 'timer',
609 'tooltip',
610 'treeitem'
611 ];
612
613 /**
614 * List of ARIA roles that define composite widgets.
615 * For more info, see: http://www.w3.org/TR/wai-aria/roles#widget_roles
616 * @type {Array.<string>}
617 */
618 Context['compositeWidgetRoles_'] = [
619 'combobox',
620 'grid',
621 'listbox',
622 'menu',
623 'menubar',
624 'radiogroup',
625 'tablist',
626 'tree',
627 'treegrid'
628 ];
629
630 /**
631 * Mutation record handling
632 */
633
634 /**
635 * Records a DOM mutation.
636 * @param {string} mutationId The id of the mutated DOM element.
637 * @param {number} mutationTime The time of the mutation.
638 */
639 Context.recordMutation = function(mutationId, mutationTime) {
640 Context.mutatedElements[mutationId] = mutationTime;
641 Context.mutationCount += 1;
642 };
643
644 /**
645 * Clears the record of a DOM mutation.
646 * @param {string} mutationId The id of the mutated DOM element.
647 */
648 Context.clearMutation = function(mutationId) {
649 delete Context.mutatedElements[mutationId];
650 Context.mutationCount -= 1;
651 };
652
653 /**
654 * Performs some operation on all recorded mutations, passing the mutated node
655 * id and mutation time into func.
656 * @param {function} func The function to apply to the recorded mutations.
657 */
658 Context.processMutations = function(func) {
659 for (mutationId in Context.mutatedElements) {
660 var mutationTime = Context.mutatedElements[mutationId];
661 if (func(mutationId, mutationTime)) {
662 break;
663 }
664 }
665 };
666
667 /**
668 * Returns whether the selection is valid to trigger a contextual search.
669 * An invalid selection is a selection that is either too long or contains a
670 * single latin character (there are some site that use x's or o's as crosses or
671 * circles), or is included or contains invalid elements.
672 * @param {selection} selection The current selection to test.
673 * @return {boolean} Whether selection should trigger contextual search.
674 */
675 Context.isSelectionValid = function(selection) {
676 var selectionText = selection.toString();
677 var length = selectionText.length;
678 if (length > Context.MAX_NUMBER_OF_CHARS_IN_SELECTION) {
679 return false;
680 }
681 if (length == 1 && selectionText.codePointAt(0) < 256) {
682 return false;
683 }
684
685 var rangeCount = selection.rangeCount;
686 for (var rangeIndex = 0; rangeIndex < rangeCount; rangeIndex++) {
687 // Test if the selection is inside an invalid element.
688 var range = window.getSelection().getRangeAt(rangeIndex);
689 var element = range.commonAncestorContainer;
690 while (element) {
691 if (element.nodeType == element.ELEMENT_NODE &&
692 !Context.isValidElement(element)) {
693 return false;
694 }
695 element = element.parentElement;
696 }
697
698 // Test if the selection contains an invalid element.
699 var startNode = range.startContainer.childNodes[range.startOffset] ||
700 range.startContainer;
701 var endNode = range.endContainer.childNodes[range.endOffset] ||
702 range.endContainer;
703 element = startNode;
704 while (element) {
705 if (element.nodeType == element.ELEMENT_NODE &&
706 !Context.isValidElement(element)) {
707 return false;
708 }
709 element = Context.getNextNode(element, endNode, false);
710 }
711 }
712 return true;
713 };
714
715 /**
716 * Forwards the selection changed notification to the controller class.
717 */
718 Context.selectionChanged = function() {
719 var newSelection = window.getSelection();
720 if (!newSelection.toString()) {
721 Context.previousSelection = null;
722 return;
723 }
724 var updated = false;
725 if (Context.previousSelection) {
726 updated =
727 (Context.previousSelection.anchorNode == newSelection.anchorNode &&
728 Context.previousSelection.anchorOffset == newSelection.anchorOffset) ||
729 (Context.previousSelection.focusNode == newSelection.focusNode &&
730 Context.previousSelection.focusOffset == newSelection.focusOffset);
731 }
732 var selectionText = newSelection.toString();
733 var valid = true;
734 if (!Context.isSelectionValid(newSelection)) {
735 // Mark selection as invalid.
736 selectionText = '';
737 valid = false;
738 }
739
740 __gCrWeb.message.invokeOnHost(
741 {'command' : 'contextualSearch.selectionChanged',
742 'text' : selectionText,
743 'updated' : updated,
744 'valid' : valid
745 });
746
747 // Snapshot the selection for comparison.
748 Context.previousSelection = {
749 'anchorNode' : newSelection.anchorNode,
750 'anchorOffset' : newSelection.anchorOffset,
751 'focusNode' : newSelection.focusNode,
752 'focusOffset' : newSelection.focusOffset
753 };
754 };
755
756 /**
757 * Gets the data necessary to create a Contextual Search from a given point
758 * in the window.
759 * @param {number} x The point's x coordinate.
760 * @param {number} y The point's y coordinate.
761 * @return {ContextData} The object describing the context.
762 */
763 Context.getContextDataFromPoint = function(x, y) {
764 var contextData = Context.contextFromPoint(x, y);
765
766 if (contextData.error) {
767 return contextData;
768 }
769 var range = contextData.range = Context.getWordRangeFromPoint(x, y);
770 if (range) {
771 contextData.selectedText = range.toString();
772 contextData.url = location.href;
773 Context.extractSurroundingDataFromRange(contextData, range);
774 Context.highlightRange = range;
775 }
776
777 return contextData;
778 };
779
780 /**
781 * Checks whether the context in a given point is valid. A context will be
782 * valid when the element at the given point is not interactive or editable.
783 * @param {number} x The point's x coordinate.
784 * @param {number} y The point's y coordinate.
785 * @return {ContextData} Context data from the point, an error set if invalid.
786 * @private
787 */
788 Context.contextFromPoint = function(x, y) {
789 // Should this use core.js's elementFromPoint_() instead?
790 var contextData = new ContextData();
791 var element = document.elementFromPoint(x, y);
792 if (!element) {
793 contextData.error = "Failed: Couldn't locate an element at " + x + ', ' + y;
794 return contextData;
795 }
796
797 var d = new Date();
798 var lastDOM = d.getTime() - Context.lastDOMMutationMillisecond;
799 if (lastDOM <= Context.DOMMutationTimeoutMillisecond) {
800 Context.processMutations(function(mutationId, mutationTime) {
801 var mutatedElement = document.getElementById(mutationId);
802 if (!mutatedElement) {
803 Context.clearMutation(mutationId);
804 } else {
805 var lastElementMutation = d.getTime() - mutationTime;
806 if (lastElementMutation < 0 ||
807 (lastElementMutation > Context.DOMMutationTimeoutMillisecond)) {
808 return false; // mutation expired, continue.
809 }
810 if (element.contains(mutatedElement) ||
811 mutatedElement.contains(element)) {
812 contextData.error = 'Failed: Tap was in element mutated ' +
813 lastElementMutation + 'ms ago (<' +
814 Context.DOMMutationTimeoutMillisecond + 'ms interval)';
815 return true; // break from processing mutations
816 }
817 }
818 return false; // continue processing mutations
819 });
820 if (contextData.error)
821 return contextData;
822 }
823
824 while (element) {
825 if (element.nodeType == element.ELEMENT_NODE &&
826 !Context.isValidElement(element, false)) {
827 contextData.error =
828 'Failed: Tap was in an invalid (' + element.nodeName + ') element';
829 return contextData;
830 }
831 element = element.parentElement;
832 }
833
834 return contextData;
835 };
836
837 /**
838 * Checks whether the given element can be used as a touch target.
839 * @see Context.isValidContextFromPoint_
840 * @param {Element} element The element in question.
841 * @param {boolean} forDisplay Whether we are testing if an element is valid
842 * for tap handling (false) or for display (true).
843 * @return {boolean} Whether the element is a valid context.
844 * @private
845 */
846 Context.isValidElement = function(element, forDisplay) {
847
848 if (element.nodeName == 'A') {
849 return forDisplay;
850 }
851
852 if (Context.invalidElements_.indexOf(element.nodeName) != -1) {
853 __gCrWeb['contextualSearch'].cxlog(
854 'Failed: ' + element.nodeName + ' element was invalid');
855 return false;
856 }
857
858 if (element.getAttribute('contenteditable')) {
859 __gCrWeb['contextualSearch'].cxlog(
860 'Failed: ' + element.nodeName + ' element was editable');
861 return false;
862 }
863
864 var role = element.getAttribute('role');
865 if (Context.widgetRoles_.indexOf(role) != -1 ||
866 Context.compositeWidgetRoles_.indexOf(role) != -1) {
867 __gCrWeb['contextualSearch'].cxlog(
868 'Failed: ' + element.nodeName + ' role ' + role + ' was invalid');
869 return false;
870 }
871
872 if (forDisplay) {
873 var style = window.getComputedStyle(element);
874 if (style.display === 'none') {
875 __gCrWeb['contextualSearch'].cxlog(
876 'Failed: ' + element.nodeName + ' hidden');
877 return false;
878 }
879 }
880
881 return true;
882 };
883
884 /**
885 * Gets the word range located at a given point. This method will find the
886 * word whose bounding rectangle contains the given point.
887 * @param {number} x The point's x coordinate.
888 * @param {number} y The point's y coordinate.
889 * @return {Range} The word range at the given point.
890 * @private
891 */
892 Context.getWordRangeFromPoint = function(x, y) {
893 var element = document.elementFromPoint(x, y);
894 var range = null;
895 try {
896 range = Context.findWordRangeFromPointRecursive(element, x, y);
897 } catch (e) {
898 __gCrWeb['contextualSearch'].cxlog(
899 'Recursive word find failed: ' + e.message);
900 }
901
902 return range;
903 };
904
905 /**
906 * Recursively gets the word range located at a given point.
907 * @see Context.getWordRangeFromPoint_
908 * @param {Node} node The node being inspected.
909 * @param {number} x The point's x coordinate.
910 * @param {number} y The point's y coordinate.
911 * @return {Range} The word range at the given point.
912 * @private
913 */
914 Context.findWordRangeFromPointRecursive = function(node, x, y) {
915 if (!node) {
916 return null;
917 }
918
919 if (node.nodeType == node.TEXT_NODE) {
920 var position = Context.findCharacterPositionInTextFromPoint(node, x, y);
921 if (position == -1) {
922 return null;
923 }
924
925 var range = node.ownerDocument.createRange();
926 range.setStart(node, position);
927 range.setEnd(node, position);
928 range.expand('word');
929
930 if (Context.rangeContainsPoint(range, x, y)) {
931 return range;
932 }
933
934 if (range) {
935 range.detach();
936 }
937 } else if (node.nodeType == node.ELEMENT_NODE) {
938 var childNodes = node.childNodes;
939 var childNodesLength = childNodes.length;
940 for (var i = 0, length = childNodesLength; i < length; i++) {
941 var childNode = childNodes[i];
942 var range = childNode.ownerDocument.createRange();
943 range.selectNodeContents(childNode);
944
945 if (Context.rangeContainsPoint(range, x, y)) {
946 range.detach();
947 return Context.findWordRangeFromPointRecursive(childNode, x, y);
948 } else {
949 range.detach();
950 }
951 }
952 }
953
954 return null;
955 };
956
957 /**
958 * Gets the position of the character range located at a given point. This
959 * method will find the single character whose bounding rectangle contains
960 * the given point and return the position of that character in the text
961 * node string. If not character is found this method returns -1.
962 * @see Context.findWordRangeFromPointRecursive_
963 * @param {number} x The point's x coordinate.
964 * @param {number} y The point's y coordinate.
965 * @param {Text} node The text node being inspected.
966 * @return {number} The position of the character in the text node string.
967 * @private
968 */
969 Context.findCharacterPositionInTextFromPoint = function(node, x, y) {
970 var startTime = new Date().getTime();
971
972 var start = 0;
973 var end = node.textContent.length - 1;
974
975 // Performs a binary search to find a single character whose bouding
976 // rectangle contains the given point.
977 var range = document.createRange();
978 while (!found || (end - start + 1) > 1) {
979 if ((new Date().getTime() - startTime) > Context.GET_RANGE_TIMEOUT_MS) {
980 __gCrWeb['contextualSearch'].cxlog('Timed out!');
981 break;
982 }
983
984 var middle = Math.floor((start + end) / 2);
985 range.setStart(node, start);
986 range.setEnd(node, middle + 1); // + 1 because end point is non-inclusive.
987
988 var found = Context.rangeContainsPoint(range, x, y);
989 if (found) {
990 end = middle;
991 } else {
992 start = middle + 1;
993 }
994 }
995
996 if (found) {
997 var text = range.toString();
998 // Tests if the character is actually a word character (a letter, digit,
999 // underscore, or any Unicode letter). If the character is not a word
1000 // character it means the given point is in a whitespace, punctuation or
1001 // other non-relevant characters, and in this case it should not be
1002 // considered a successful finding.
1003 if (!Context.reWordCharacter_.test(text)) {
1004 found = false;
1005 }
1006 }
1007
1008 range.detach();
1009
1010 return found ? start : -1;
1011 };
1012
1013 /**
1014 * Gets the surrounding data from a given range.
1015 * @param {ContextData} contextData Object where the data will be written.
1016 * @param {Range} range A text range.
1017 * @private
1018 */
1019 Context.extractSurroundingDataFromRange = function(contextData, range) {
1020 var surroundingRange = range.cloneRange();
1021 var length = surroundingRange.toString().length;
1022 while (length < Context.NUMBER_OF_CHARS_IN_SURROUNDING_SENTENCES) {
1023 surroundingRange.expand('sentence');
1024 var oldLength = length;
1025 length = surroundingRange.toString().length;
1026 if (oldLength == length) {
1027 break;
1028 }
1029 }
1030
1031 var textNodeType = range.startContainer.TEXT_NODE;
1032 var selectionStartOffset = 0;
1033 var selectionStartNode = range.startContainer;
1034 if (range.startContainer.nodeType == textNodeType) {
1035 selectionStartOffset = range.startOffset;
1036 } else {
1037 selectionStartNode = range.startContainer.childNodes[range.startOffset];
1038 }
1039
1040 var surroundingStartOffset = 0;
1041
1042 if (surroundingRange.startContainer.nodeType == textNodeType) {
1043 surroundingStartOffset = surroundingRange.startOffset;
1044 }
1045 var surroundingRemoveAtEnd = 0;
1046 if (surroundingRange.endContainer.nodeType == textNodeType) {
1047 surroundingRemoveAtEnd = surroundingRange.endContainer.textContent.length -
1048 surroundingRange.endOffset;
1049 }
1050
1051 // It is possible that invalid nodes are present inside the surrounding range.
1052 // Extract text to make sure this is not the case.
1053 var textNodes = Context.textNodesFromRange(surroundingRange);
1054
1055 var offset = 0;
1056 var index = 0;
1057 var surroundingString = '';
1058 var foundStart = false;
1059 for (index = 0; index < textNodes.length; index++) {
1060 if (textNodes[index] === ' ') {
1061 surroundingString += ' ';
1062 if (!foundStart) {
1063 offset += 1;
1064 }
1065 continue;
1066 }
1067 if (textNodes[index] == selectionStartNode) {
1068 foundStart = true;
1069 }
1070 if (!foundStart) {
1071 offset += textNodes[index].textContent.length;
1072 }
1073 surroundingString += textNodes[index].textContent;
1074 }
1075 offset += selectionStartOffset - surroundingStartOffset;
1076
1077 surroundingString = surroundingString.substring(surroundingStartOffset,
1078 surroundingString.length - surroundingRemoveAtEnd);
1079
1080 contextData.surroundingText = surroundingString;
1081 contextData.offsetStart = offset;
1082 contextData.offsetEnd = offset + range.toString().length;
1083
1084 Context.surroundingRange = {
1085 startContainer: surroundingRange.startContainer,
1086 startOffset: surroundingRange.startOffset,
1087 endContainer: surroundingRange.endContainer,
1088 endOffset: surroundingRange.endOffset,
1089 highlightStartOffset: contextData.offsetStart,
1090 highlightEndOffset: contextData.offsetEnd
1091 };
1092
1093 surroundingRange.detach();
1094 };
1095
1096 /**
1097 * Returns whether character is a whitespace.
1098 * @param {string} character The character to test.
1099 * @return {boolean} Whether |character| is a whitespace.
1100 */
1101 Context.isCharSpace = function(character) {
1102 return character.trim().length == 0;
1103 };
1104
1105 /**
1106 * Find next node in a DFS order. A parent is returned before its children.
1107 * @param {Node} node The current node.
1108 * @param {Node} endNode The right limit of the DFS.
1109 * @param {boolean} onlyValid Whether invalid node should be skipped in DFS.
1110 * @return {Node} The node coming after |node|. Null is endNode is reached.
1111 */
1112 Context.getNextNode = function(node, endNode, onlyValid) {
1113 if (!node)
1114 return null;
1115
1116 if (node.childNodes.length > 0) {
1117 node = node.childNodes[0];
1118 if (node.nodeType == node.TEXT_NODE ||
1119 (node.nodeType == node.ELEMENT_NODE &&
1120 (!onlyValid || Context.isValidElement(node, true)))) {
1121 return node;
1122 }
1123 if (node.nodeType == node.ELEMENT_NODE && node.contains(endNode)) {
1124 return null;
1125 }
1126 }
1127
1128 while (node != null) {
1129 if (!node.nextSibling) {
1130 node = node.parentNode;
1131 continue;
1132 }
1133 if (node.contains(endNode))
1134 return null;
1135 node = node.nextSibling;
1136 if (node.nodeType == node.TEXT_NODE ||
1137 (node.nodeType == node.ELEMENT_NODE &&
1138 (!onlyValid || Context.isValidElement(node, true)))) {
1139 return node;
1140 }
1141 }
1142 return null;
1143 };
1144
1145 /**
1146 * Creates the list of text nodes that are part of range. The list will contain
1147 * whitespace strings to replace block elements.
1148 * @param {Range} range The range containing the text information.
1149 * @return {Array.<Node>} The array of text nodes in the range.
1150 */
1151 Context.textNodesFromRange = function(range) {
1152 var blockStack = [];
1153
1154 var startNode = range.startContainer.childNodes[range.startOffset] ||
1155 range.startContainer;
1156 var endNode = range.endContainer.childNodes[range.endOffset] ||
1157 range.endContainer;
1158
1159 if (startNode == endNode && startNode.childNodes.length === 0) {
1160 return [startNode];
1161 }
1162
1163 var textNodes = [];
1164 var node = startNode;
1165 // Do not add a space as first node.
1166 var addSpace = false;
1167 var lastNodeAddedHasSpace = true;
1168
1169 do {
1170 if (node.nodeType == node.TEXT_NODE && node.textContent.length > 0) {
1171 while (blockStack.length && !blockStack[0].contains(node)) {
1172 addSpace = true;
1173 blockStack.shift();
1174 }
1175 if (addSpace && !lastNodeAddedHasSpace &&
1176 !Context.isCharSpace(node.textContent[0])) {
1177 textNodes.push(' ');
1178 addSpace = false;
1179 }
1180 textNodes.push(node);
1181 lastNodeAddedHasSpace = Context.isCharSpace(
1182 node.textContent[node.textContent.length - 1]);
1183 } else if (node.nodeType == node.ELEMENT_NODE) {
1184 var style = window.getComputedStyle(node);
1185 if (style && style.display != 'inline') {
1186 addSpace = true;
1187 blockStack.unshift(node);
1188 }
1189 }
1190 node = Context.getNextNode(node, endNode, true);
1191 } while (node && node != endNode);
1192
1193 if (node == endNode && node.nodeType == node.TEXT_NODE) {
1194 if (addSpace && !lastNodeAddedHasSpace &&
1195 !Context.isCharSpace(node.textContent[0])) {
1196 textNodes.push(' ');
1197 }
1198 textNodes.push(node);
1199 }
1200 return textNodes;
1201 };
1202
1203 /**
1204 * Checks if a particular range contains a given point.
1205 * @param {Range} range A text range.
1206 * @param {number} x The point's x coordinate.
1207 * @param {number} y The point's y coordinate.
1208 * @return {boolean} whether the range contains the given point.
1209 */
1210 Context.rangeContainsPoint = function(range, x, y) {
1211 var rects = range.getClientRects();
1212 var rectsLength = rects.length;
1213 for (var i = 0, length = rectsLength; i < length; i++) {
1214 var rect = rects[i];
1215 var contains = Context.rectContainsPoint(rect, x, y);
1216 if (contains) {
1217 return true;
1218 }
1219 }
1220 return false;
1221 };
1222
1223 /**
1224 * Checks if a particular rectangle contains a given point.
1225 * @param {Rect} rect A rectangle.
1226 * @param {number} x The point's x coordinate.
1227 * @param {number} y The point's y coordinate.
1228 * @return {boolean} whether the rectangle contains the given point.
1229 * @private
1230 */
1231 Context.rectContainsPoint = function(rect, x, y) {
1232 if (x >= rect.left && x <= rect.right &&
1233 y >= rect.top && y <= rect.bottom) {
1234 return true;
1235 }
1236 return false;
1237 };
1238
1239 /**
1240 * Create a new range to the [startOffset, endOffset] in the surrounding
1241 * range returned by Context.extractSurroundingDataFromRange.
1242 * @param {number} startOffset first character to include in the range.
1243 * @param {number} endOffset last character to include in the range.
1244 * @return {Range} the range including [startOffset, endOffset].
1245 * @private
1246 */
1247 Context.createSurroundingRange = function(startOffset, endOffset) {
1248 if ((startOffset == Context.surroundingRange.highlightStartOffset &&
1249 endOffset == Context.surroundingRange.highlightEndOffset) ||
1250 startOffset >= endOffset) {
1251 return;
1252 }
1253 var range = document.createRange();
1254 range.setStart(Context.surroundingRange.startContainer,
1255 Context.surroundingRange.startOffset);
1256 range.setEnd(Context.surroundingRange.endContainer,
1257 Context.surroundingRange.endOffset);
1258
1259 var textNodes = Context.textNodesFromRange(range);
1260 var highlightRange = document.createRange();
1261 var length = textNodes.length;
1262 var offset = 0;
1263 if (length > 0 && Context.surroundingRange.startContainer == textNodes[0]) {
1264 // Ignore the text inside |startContainer| before the range.
1265 offset -= Context.surroundingRange.startOffset;
1266 }
1267 var startSet = false;
1268 for (var index = 0; index < length; index++) {
1269 var node = textNodes[index];
1270 if (node === ' ') {
1271 offset += 1;
1272 continue;
1273 }
1274 if (!startSet && offset + node.textContent.length > startOffset) {
1275 startSet = true;
1276 highlightRange.setStart(node, startOffset - offset);
1277 }
1278 if (offset + node.textContent.length >= endOffset) {
1279 highlightRange.setEnd(node, endOffset - offset);
1280 break;
1281 }
1282 offset += node.textContent.length;
1283 }
1284 return highlightRange;
1285 };
1286
1287 /**
1288 * Returns the string contained in the parameter |range|. Text rules are the
1289 * same as textNodesFromRange.
1290 * @param {Range} range the range to convert to string.
1291 * @return {string} the string contained in range.
1292 * @private
1293 */
1294 Context.rangeToString = function(range) {
1295 var textNodes = Context.textNodesFromRange(range);
1296 var string = '';
1297 var length = textNodes.length;
1298 for (var index = 0; index < length; index++) {
1299 var node = textNodes[index];
1300 if (node === ' ') {
1301 string += ' ';
1302 continue;
1303 }
1304 var nodeString = node.textContent;
1305 if (node == range.endNode) {
1306 nodeString = nodeString.substring(0, range.endOffset);
1307 }
1308 if (node == range.startNode) {
1309 nodeString.substring(range.startOffset, nodeString.length);
1310 }
1311 string += nodeString;
1312 }
1313 return string;
1314 };
1315
1316 /**
1317 * Create the text client rects contained in the current |highlightRange|.
1318 * @return {string} A string containing a comma separated list of rects in
1319 * the format 'top bottom left right'. Coordinates are page based (not screen
1320 * based).
1321 * @private
1322 */
1323 Context.getHighlightRects = function() {
1324 if (Context.highlightRange == null) {
1325 return '';
1326 }
1327 var rectsArray = new Array();
1328 var rects = Context.highlightRange.getClientRects();
1329 var rectsLength = rects.length;
1330 for (var i = 0, length = rectsLength; i < length; i++) {
1331 var top = rects[i].top + document.body.scrollTop;
1332 var bottom = rects[i].bottom + document.body.scrollTop;
1333 var left = rects[i].left + document.body.scrollLeft;
1334 var right = rects[i].right + document.body.scrollLeft;
1335 rectsArray[i] = '' + top + ' ' + bottom + ' ' + left + ' ' + right;
1336 }
1337 return rectsArray.join(',');
1338 };
1339
1340 /* Anyonymizing block end */
1341 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698