|  | // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | /** | 
|  | * EventsView displays a filtered list of all events sharing a source, and | 
|  | * a details pane for the selected sources. | 
|  | * | 
|  | *  +----------------------++----------------+ | 
|  | *  |      filter box      ||                | | 
|  | *  +----------------------+|                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |     source list      ||    details     | | 
|  | *  |                      ||    view        | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  |                      ||                | | 
|  | *  +----------------------++----------------+ | 
|  | */ | 
|  | var EventsView = (function() { | 
|  | 'use strict'; | 
|  |  | 
|  | // How soon after updating the filter list the counter should be updated. | 
|  | var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; | 
|  |  | 
|  | // We inherit from View. | 
|  | var superClass = View; | 
|  |  | 
|  | /* | 
|  | * @constructor | 
|  | */ | 
|  | function EventsView() { | 
|  | assertFirstConstructorCall(EventsView); | 
|  |  | 
|  | // Call superclass's constructor. | 
|  | superClass.call(this); | 
|  |  | 
|  | // Initialize the sub-views. | 
|  | var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID), | 
|  | new DivView(EventsView.LIST_BOX_ID)); | 
|  |  | 
|  | this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID); | 
|  |  | 
|  | this.splitterView_ = new ResizableVerticalSplitView( | 
|  | leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID)); | 
|  |  | 
|  | SourceTracker.getInstance().addSourceEntryObserver(this); | 
|  |  | 
|  | this.tableBody_ = $(EventsView.TBODY_ID); | 
|  |  | 
|  | this.filterInput_ = $(EventsView.FILTER_INPUT_ID); | 
|  | this.filterCount_ = $(EventsView.FILTER_COUNT_ID); | 
|  |  | 
|  | this.filterInput_.addEventListener('search', | 
|  | this.onFilterTextChanged_.bind(this), true); | 
|  |  | 
|  | $(EventsView.SELECT_ALL_ID).addEventListener( | 
|  | 'click', this.selectAll_.bind(this), true); | 
|  |  | 
|  | $(EventsView.SORT_BY_ID_ID).addEventListener( | 
|  | 'click', this.sortById_.bind(this), true); | 
|  |  | 
|  | $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener( | 
|  | 'click', this.sortBySourceType_.bind(this), true); | 
|  |  | 
|  | $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener( | 
|  | 'click', this.sortByDescription_.bind(this), true); | 
|  |  | 
|  | new MouseOverHelp(EventsView.FILTER_HELP_ID, | 
|  | EventsView.FILTER_HELP_HOVER_ID); | 
|  |  | 
|  | // Sets sort order and filter. | 
|  | this.setFilter_(''); | 
|  |  | 
|  | this.initializeSourceList_(); | 
|  | } | 
|  |  | 
|  | EventsView.TAB_ID = 'tab-handle-events'; | 
|  | EventsView.TAB_NAME = 'Events'; | 
|  | EventsView.TAB_HASH = '#events'; | 
|  |  | 
|  | // IDs for special HTML elements in events_view.html | 
|  | EventsView.TBODY_ID = 'events-view-source-list-tbody'; | 
|  | EventsView.FILTER_INPUT_ID = 'events-view-filter-input'; | 
|  | EventsView.FILTER_COUNT_ID = 'events-view-filter-count'; | 
|  | EventsView.FILTER_HELP_ID = 'events-view-filter-help'; | 
|  | EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover'; | 
|  | EventsView.SELECT_ALL_ID = 'events-view-select-all'; | 
|  | EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id'; | 
|  | EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source'; | 
|  | EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description'; | 
|  | EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box'; | 
|  | EventsView.TOPBAR_ID = 'events-view-filter-box'; | 
|  | EventsView.LIST_BOX_ID = 'events-view-source-list'; | 
|  | EventsView.SIZER_ID = 'events-view-splitter-box'; | 
|  |  | 
|  | cr.addSingletonGetter(EventsView); | 
|  |  | 
|  | EventsView.prototype = { | 
|  | // Inherit the superclass's methods. | 
|  | __proto__: superClass.prototype, | 
|  |  | 
|  | /** | 
|  | * Initializes the list of source entries.  If source entries are already, | 
|  | * being displayed, removes them all in the process. | 
|  | */ | 
|  | initializeSourceList_: function() { | 
|  | this.currentSelectedRows_ = []; | 
|  | this.sourceIdToRowMap_ = {}; | 
|  | this.tableBody_.innerHTML = ''; | 
|  | this.numPrefilter_ = 0; | 
|  | this.numPostfilter_ = 0; | 
|  | this.invalidateFilterCounter_(); | 
|  | this.invalidateDetailsView_(); | 
|  | }, | 
|  |  | 
|  | setGeometry: function(left, top, width, height) { | 
|  | superClass.prototype.setGeometry.call(this, left, top, width, height); | 
|  | this.splitterView_.setGeometry(left, top, width, height); | 
|  | }, | 
|  |  | 
|  | show: function(isVisible) { | 
|  | superClass.prototype.show.call(this, isVisible); | 
|  | this.splitterView_.show(isVisible); | 
|  | }, | 
|  |  | 
|  | getFilterText_: function() { | 
|  | return this.filterInput_.value; | 
|  | }, | 
|  |  | 
|  | setFilterText_: function(filterText) { | 
|  | this.filterInput_.value = filterText; | 
|  | this.onFilterTextChanged_(); | 
|  | }, | 
|  |  | 
|  | onFilterTextChanged_: function() { | 
|  | this.setFilter_(this.getFilterText_()); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Updates text in the details view when privacy stripping is toggled. | 
|  | */ | 
|  | onPrivacyStrippingChanged: function() { | 
|  | this.invalidateDetailsView_(); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Updates text in the details view when time display mode is toggled. | 
|  | */ | 
|  | onUseRelativeTimesChanged: function() { | 
|  | this.invalidateDetailsView_(); | 
|  | }, | 
|  |  | 
|  | comparisonFuncWithReversing_: function(a, b) { | 
|  | var result = this.comparisonFunction_(a, b); | 
|  | if (this.doSortBackwards_) | 
|  | result *= -1; | 
|  | return result; | 
|  | }, | 
|  |  | 
|  | sort_: function() { | 
|  | var sourceEntries = []; | 
|  | for (var id in this.sourceIdToRowMap_) { | 
|  | sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry()); | 
|  | } | 
|  | sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); | 
|  |  | 
|  | // Reposition source rows from back to front. | 
|  | for (var i = sourceEntries.length - 2; i >= 0; --i) { | 
|  | var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()]; | 
|  | var nextSourceId = sourceEntries[i + 1].getSourceId(); | 
|  | if (sourceRow.getNextNodeSourceId() != nextSourceId) { | 
|  | var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; | 
|  | sourceRow.moveBefore(nextSourceRow); | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | setFilter_: function(filterText) { | 
|  | var lastComparisonFunction = this.comparisonFunction_; | 
|  | var lastDoSortBackwards = this.doSortBackwards_; | 
|  |  | 
|  | var filterParser = new SourceFilterParser(filterText); | 
|  | this.currentFilter_ = filterParser.filter; | 
|  |  | 
|  | this.pickSortFunction_(filterParser.sort); | 
|  |  | 
|  | if (lastComparisonFunction != this.comparisonFunction_ || | 
|  | lastDoSortBackwards != this.doSortBackwards_) { | 
|  | this.sort_(); | 
|  | } | 
|  |  | 
|  | // Iterate through all of the rows and see if they match the filter. | 
|  | for (var id in this.sourceIdToRowMap_) { | 
|  | var entry = this.sourceIdToRowMap_[id]; | 
|  | entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry())); | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Given a "sort" object with "method" and "backwards" keys, looks up and | 
|  | * sets |comparisonFunction_| and |doSortBackwards_|.  If the ID does not | 
|  | * correspond to a sort function, defaults to sorting by ID. | 
|  | */ | 
|  | pickSortFunction_: function(sort) { | 
|  | this.doSortBackwards_ = sort.backwards; | 
|  | this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method]; | 
|  | if (!this.comparisonFunction_) { | 
|  | this.doSortBackwards_ = false; | 
|  | this.comparisonFunction_ = compareSourceId_; | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Repositions |sourceRow|'s in the table using an insertion sort. | 
|  | * Significantly faster than sorting the entire table again, when only | 
|  | * one entry has changed. | 
|  | */ | 
|  | insertionSort_: function(sourceRow) { | 
|  | // SourceRow that should be after |sourceRow|, if it needs | 
|  | // to be moved earlier in the list. | 
|  | var sourceRowAfter = sourceRow; | 
|  | while (true) { | 
|  | var prevSourceId = sourceRowAfter.getPreviousNodeSourceId(); | 
|  | if (prevSourceId == null) | 
|  | break; | 
|  | var prevSourceRow = this.sourceIdToRowMap_[prevSourceId]; | 
|  | if (this.comparisonFuncWithReversing_( | 
|  | sourceRow.getSourceEntry(), | 
|  | prevSourceRow.getSourceEntry()) >= 0) { | 
|  | break; | 
|  | } | 
|  | sourceRowAfter = prevSourceRow; | 
|  | } | 
|  | if (sourceRowAfter != sourceRow) { | 
|  | sourceRow.moveBefore(sourceRowAfter); | 
|  | return; | 
|  | } | 
|  |  | 
|  | var sourceRowBefore = sourceRow; | 
|  | while (true) { | 
|  | var nextSourceId = sourceRowBefore.getNextNodeSourceId(); | 
|  | if (nextSourceId == null) | 
|  | break; | 
|  | var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; | 
|  | if (this.comparisonFuncWithReversing_( | 
|  | sourceRow.getSourceEntry(), | 
|  | nextSourceRow.getSourceEntry()) <= 0) { | 
|  | break; | 
|  | } | 
|  | sourceRowBefore = nextSourceRow; | 
|  | } | 
|  | if (sourceRowBefore != sourceRow) | 
|  | sourceRow.moveAfter(sourceRowBefore); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Called whenever SourceEntries are updated with new log entries.  Updates | 
|  | * the corresponding table rows, sort order, and the details view as needed. | 
|  | */ | 
|  | onSourceEntriesUpdated: function(sourceEntries) { | 
|  | var isUpdatedSourceSelected = false; | 
|  | var numNewSourceEntries = 0; | 
|  |  | 
|  | for (var i = 0; i < sourceEntries.length; ++i) { | 
|  | var sourceEntry = sourceEntries[i]; | 
|  |  | 
|  | // Lookup the row. | 
|  | var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()]; | 
|  |  | 
|  | if (!sourceRow) { | 
|  | sourceRow = new SourceRow(this, sourceEntry); | 
|  | this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow; | 
|  | ++numNewSourceEntries; | 
|  | } else { | 
|  | sourceRow.onSourceUpdated(); | 
|  | } | 
|  |  | 
|  | if (sourceRow.isSelected()) | 
|  | isUpdatedSourceSelected = true; | 
|  |  | 
|  | // TODO(mmenke): Fix sorting when sorting by duration. | 
|  | //               Duration continuously increases for all entries that | 
|  | //               are still active.  This can result in incorrect | 
|  | //               sorting, until sort_ is called. | 
|  | this.insertionSort_(sourceRow); | 
|  | } | 
|  |  | 
|  | if (isUpdatedSourceSelected) | 
|  | this.invalidateDetailsView_(); | 
|  | if (numNewSourceEntries) | 
|  | this.incrementPrefilterCount(numNewSourceEntries); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Returns the SourceRow with the specified ID, if there is one. | 
|  | * Otherwise, returns undefined. | 
|  | */ | 
|  | getSourceRow: function(id) { | 
|  | return this.sourceIdToRowMap_[id]; | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Called whenever all log events are deleted. | 
|  | */ | 
|  | onAllSourceEntriesDeleted: function() { | 
|  | this.initializeSourceList_(); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Called when either a log file is loaded, after clearing the old entries, | 
|  | * but before getting any new ones. | 
|  | */ | 
|  | onLoadLogStart: function() { | 
|  | // Needed to sort new sourceless entries correctly. | 
|  | this.maxReceivedSourceId_ = 0; | 
|  | }, | 
|  |  | 
|  | onLoadLogFinish: function(data) { | 
|  | return true; | 
|  | }, | 
|  |  | 
|  | incrementPrefilterCount: function(offset) { | 
|  | this.numPrefilter_ += offset; | 
|  | this.invalidateFilterCounter_(); | 
|  | }, | 
|  |  | 
|  | incrementPostfilterCount: function(offset) { | 
|  | this.numPostfilter_ += offset; | 
|  | this.invalidateFilterCounter_(); | 
|  | }, | 
|  |  | 
|  | onSelectionChanged: function() { | 
|  | this.invalidateDetailsView_(); | 
|  | }, | 
|  |  | 
|  | clearSelection: function() { | 
|  | var prevSelection = this.currentSelectedRows_; | 
|  | this.currentSelectedRows_ = []; | 
|  |  | 
|  | // Unselect everything that is currently selected. | 
|  | for (var i = 0; i < prevSelection.length; ++i) { | 
|  | prevSelection[i].setSelected(false); | 
|  | } | 
|  |  | 
|  | this.onSelectionChanged(); | 
|  | }, | 
|  |  | 
|  | selectAll_: function(event) { | 
|  | for (var id in this.sourceIdToRowMap_) { | 
|  | var sourceRow = this.sourceIdToRowMap_[id]; | 
|  | if (sourceRow.isMatchedByFilter()) { | 
|  | sourceRow.setSelected(true); | 
|  | } | 
|  | } | 
|  | event.preventDefault(); | 
|  | }, | 
|  |  | 
|  | unselectAll_: function() { | 
|  | var entries = this.currentSelectedRows_.slice(0); | 
|  | for (var i = 0; i < entries.length; ++i) { | 
|  | entries[i].setSelected(false); | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * If |params| includes a query, replaces the current filter and unselects. | 
|  | * all items.  If it includes a selection, tries to select the relevant | 
|  | * item. | 
|  | */ | 
|  | setParameters: function(params) { | 
|  | if (params.q) { | 
|  | this.unselectAll_(); | 
|  | this.setFilterText_(params.q); | 
|  | } | 
|  |  | 
|  | if (params.s) { | 
|  | var sourceRow = this.sourceIdToRowMap_[params.s]; | 
|  | if (sourceRow) { | 
|  | sourceRow.setSelected(true); | 
|  | this.scrollToSourceId(params.s); | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Scrolls to the source indicated by |sourceId|, if displayed. | 
|  | */ | 
|  | scrollToSourceId: function(sourceId) { | 
|  | this.detailsView_.scrollToSourceId(sourceId); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * If already using the specified sort method, flips direction.  Otherwise, | 
|  | * removes pre-existing sort parameter before adding the new one. | 
|  | */ | 
|  | toggleSortMethod_: function(sortMethod) { | 
|  | // Get old filter text and remove old sort directives, if any. | 
|  | var filterParser = new SourceFilterParser(this.getFilterText_()); | 
|  | var filterText = filterParser.filterTextWithoutSort; | 
|  |  | 
|  | filterText = 'sort:' + sortMethod + ' ' + filterText; | 
|  |  | 
|  | // If already using specified sortMethod, sort backwards. | 
|  | if (!this.doSortBackwards_ && | 
|  | COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) { | 
|  | filterText = '-' + filterText; | 
|  | } | 
|  |  | 
|  | this.setFilterText_(filterText.trim()); | 
|  | }, | 
|  |  | 
|  | sortById_: function(event) { | 
|  | this.toggleSortMethod_('id'); | 
|  | }, | 
|  |  | 
|  | sortBySourceType_: function(event) { | 
|  | this.toggleSortMethod_('source'); | 
|  | }, | 
|  |  | 
|  | sortByDescription_: function(event) { | 
|  | this.toggleSortMethod_('desc'); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Modifies the map of selected rows to include/exclude the one with | 
|  | * |sourceId|, if present.  Does not modify checkboxes or the LogView. | 
|  | * Should only be called by a SourceRow in response to its selection | 
|  | * state changing. | 
|  | */ | 
|  | modifySelectionArray: function(sourceId, addToSelection) { | 
|  | var sourceRow = this.sourceIdToRowMap_[sourceId]; | 
|  | if (!sourceRow) | 
|  | return; | 
|  | // Find the index for |sourceEntry| in the current selection list. | 
|  | var index = -1; | 
|  | for (var i = 0; i < this.currentSelectedRows_.length; ++i) { | 
|  | if (this.currentSelectedRows_[i] == sourceRow) { | 
|  | index = i; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (index != -1 && !addToSelection) { | 
|  | // Remove from the selection. | 
|  | this.currentSelectedRows_.splice(index, 1); | 
|  | } | 
|  |  | 
|  | if (index == -1 && addToSelection) { | 
|  | this.currentSelectedRows_.push(sourceRow); | 
|  | } | 
|  | }, | 
|  |  | 
|  | getSelectedSourceEntries_: function() { | 
|  | var sourceEntries = []; | 
|  | for (var i = 0; i < this.currentSelectedRows_.length; ++i) { | 
|  | sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry()); | 
|  | } | 
|  | return sourceEntries; | 
|  | }, | 
|  |  | 
|  | invalidateDetailsView_: function() { | 
|  | this.detailsView_.setData(this.getSelectedSourceEntries_()); | 
|  | }, | 
|  |  | 
|  | invalidateFilterCounter_: function() { | 
|  | if (!this.outstandingRepaintFilterCounter_) { | 
|  | this.outstandingRepaintFilterCounter_ = true; | 
|  | window.setTimeout(this.repaintFilterCounter_.bind(this), | 
|  | REPAINT_FILTER_COUNTER_TIMEOUT_MS); | 
|  | } | 
|  | }, | 
|  |  | 
|  | repaintFilterCounter_: function() { | 
|  | this.outstandingRepaintFilterCounter_ = false; | 
|  | this.filterCount_.innerHTML = ''; | 
|  | addTextNode(this.filterCount_, | 
|  | this.numPostfilter_ + ' of ' + this.numPrefilter_); | 
|  | } | 
|  | };  // end of prototype. | 
|  |  | 
|  | // ------------------------------------------------------------------------ | 
|  | // Helper code for comparisons | 
|  | // ------------------------------------------------------------------------ | 
|  |  | 
|  | var COMPARISON_FUNCTION_TABLE = { | 
|  | // sort: and sort:- are allowed | 
|  | '': compareSourceId_, | 
|  | 'active': compareActive_, | 
|  | 'desc': compareDescription_, | 
|  | 'description': compareDescription_, | 
|  | 'duration': compareDuration_, | 
|  | 'id': compareSourceId_, | 
|  | 'source': compareSourceType_, | 
|  | 'type': compareSourceType_ | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Sorts active entries first.  If both entries are inactive, puts the one | 
|  | * that was active most recently first.  If both are active, uses source ID, | 
|  | * which puts longer lived events at the top, and behaves better than using | 
|  | * duration or time of first event. | 
|  | */ | 
|  | function compareActive_(source1, source2) { | 
|  | if (!source1.isInactive() && source2.isInactive()) | 
|  | return -1; | 
|  | if (source1.isInactive() && !source2.isInactive()) | 
|  | return 1; | 
|  | if (source1.isInactive()) { | 
|  | var deltaEndTime = source1.getEndTicks() - source2.getEndTicks(); | 
|  | if (deltaEndTime != 0) { | 
|  | // The one that ended most recently (Highest end time) should be sorted | 
|  | // first. | 
|  | return -deltaEndTime; | 
|  | } | 
|  | // If both ended at the same time, then odds are they were related events, | 
|  | // started one after another, so sort in the opposite order of their | 
|  | // source IDs to get a more intuitive ordering. | 
|  | return -compareSourceId_(source1, source2); | 
|  | } | 
|  | return compareSourceId_(source1, source2); | 
|  | } | 
|  |  | 
|  | function compareDescription_(source1, source2) { | 
|  | var source1Text = source1.getDescription().toLowerCase(); | 
|  | var source2Text = source2.getDescription().toLowerCase(); | 
|  | var compareResult = source1Text.localeCompare(source2Text); | 
|  | if (compareResult != 0) | 
|  | return compareResult; | 
|  | return compareSourceId_(source1, source2); | 
|  | } | 
|  |  | 
|  | function compareDuration_(source1, source2) { | 
|  | var durationDifference = source2.getDuration() - source1.getDuration(); | 
|  | if (durationDifference) | 
|  | return durationDifference; | 
|  | return compareSourceId_(source1, source2); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * For the purposes of sorting by source IDs, entries without a source | 
|  | * appear right after the SourceEntry with the highest source ID received | 
|  | * before the sourceless entry. Any ambiguities are resolved by ordering | 
|  | * the entries without a source by the order in which they were received. | 
|  | */ | 
|  | function compareSourceId_(source1, source2) { | 
|  | var sourceId1 = source1.getSourceId(); | 
|  | if (sourceId1 < 0) | 
|  | sourceId1 = source1.getMaxPreviousEntrySourceId(); | 
|  | var sourceId2 = source2.getSourceId(); | 
|  | if (sourceId2 < 0) | 
|  | sourceId2 = source2.getMaxPreviousEntrySourceId(); | 
|  |  | 
|  | if (sourceId1 != sourceId2) | 
|  | return sourceId1 - sourceId2; | 
|  |  | 
|  | // One or both have a negative ID. In either case, the source with the | 
|  | // highest ID should be sorted first. | 
|  | return source2.getSourceId() - source1.getSourceId(); | 
|  | } | 
|  |  | 
|  | function compareSourceType_(source1, source2) { | 
|  | var source1Text = source1.getSourceTypeString(); | 
|  | var source2Text = source2.getSourceTypeString(); | 
|  | var compareResult = source1Text.localeCompare(source2Text); | 
|  | if (compareResult != 0) | 
|  | return compareResult; | 
|  | return compareSourceId_(source1, source2); | 
|  | } | 
|  |  | 
|  | return EventsView; | 
|  | })(); |