Web Inspector: Include a table in New Network Tab
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 25 Sep 2017 21:43:54 +0000 (21:43 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 25 Sep 2017 21:43:54 +0000 (21:43 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177206

Reviewed by Matt Baker and Brian Burg.

This includes an initial implementation of the NetworkTableContentView,
and a generic Table / TableColumn implementation ported from DataGrid.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
New strings and files.

* UserInterface/Views/NetworkGridContentView.css:
(.content-view:matches(.network, .network-grid) > .data-grid .cache-type):
(.cache-type): Deleted.
* UserInterface/Views/ResourceDetailsSidebarPanel.css:
(.sidebar > .panel.resource-details .cache-type):
Make the .cache-type selector more specific for the legacy cases.

* UserInterface/Views/NetworkTabContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/NetworkGridContentView.css.
(.content-view.network > .content-browser):
* UserInterface/Views/NetworkTabContentView.js:
(WI.NetworkTabContentView.prototype.shown):
(WI.NetworkTabContentView.prototype.hidden):
(WI.NetworkTabContentView.prototype.closed):
NetworkTabContentView has a ContentBrowser so it should be passing
ContentBrowser lifecycle events (shown, hidden, closed) through to
the sub-content browser.

* UserInterface/Views/TableColumn.js: Added.
(WI.TableColumn.prototype.get identifier):
(WI.TableColumn.prototype.get name):
(WI.TableColumn.prototype.get width):
(WI.TableColumn.prototype.get minWidth):
(WI.TableColumn.prototype.get maxWidth):
(WI.TableColumn.prototype.get hidden):
(WI.TableColumn.prototype.get defaultHidden):
(WI.TableColumn.prototype.get sortable):
(WI.TableColumn.prototype.get align):
(WI.TableColumn.prototype.get locked):
(WI.TableColumn.prototype.get flexible):
(WI.TableColumn.prototype.setWidth):
(WI.TableColumn.prototype.setHidden):
Model object for a column. Values are getter only. Columns may express
size constraints (min width / max width) that are respected as much as
possible. When a column is resized it dispatches an event.

* UserInterface/Views/Table.js: Added.
(WI.Table):
(WI.Table.prototype.get element):
(WI.Table.prototype.get identifier):
(WI.Table.prototype.get dataSource):
(WI.Table.prototype.get delegate):
(WI.Table.prototype.get rowHeight):
(WI.Table.prototype.get selectedRow):
(WI.Table.prototype.get sortOrder):
(WI.Table.prototype.set sortOrder):
(WI.Table.prototype.get sortColumnIdentifier):
(WI.Table.prototype.set sortColumnIdentifier):
(WI.Table.prototype.resize):
(WI.Table.prototype.reloadData):
(WI.Table.prototype.reloadDataAddedToEndOnly):
(WI.Table.prototype.reloadRow):
(WI.Table.prototype.reloadCell):
(WI.Table.prototype.selectRow):
(WI.Table.prototype.clearSelectedRow):
(WI.Table.prototype.columnWithIdentifier):
(WI.Table.prototype.addColumn):
(WI.Table.prototype.showColumn):
(WI.Table.prototype.hideColumn):
(WI.Table.prototype.restoreScrollPosition):
(WI.Table.prototype.initialLayout):
(WI.Table.prototype.layout):
(WI.Table.prototype.resizerDragStarted):
(WI.Table.prototype.resizerDragging.growableSize):
(WI.Table.prototype.resizerDragging.shrinkableSize):
(WI.Table.prototype.resizerDragging.canGrow):
(WI.Table.prototype.resizerDragging.canShrink):
(WI.Table.prototype.resizerDragging.columnToResize):
(WI.Table.prototype.resizerDragging):
(WI.Table.prototype.resizerDragEnded):
(WI.Table.prototype._needsLayout):
(WI.Table.prototype._createHeaderCell):
(WI.Table.prototype._createFillerCell):
(WI.Table.prototype._createCell):
(WI.Table.prototype._getOrCreateRow):
(WI.Table.prototype._populatedCellForColumnAndRow):
(WI.Table.prototype._populateRow):
(WI.Table.prototype._resizeColumnsAndFiller.distributeRemainingPixels):
(WI.Table.prototype._resizeColumnsAndFiller.bestFit):
(WI.Table.prototype._resizeColumnsAndFiller):
(WI.Table.prototype._updateVisibleRows):
(WI.Table.prototype._applyColumnWidths):
(WI.Table.prototype._positionResizerElements):
(WI.Table.prototype._isRowVisible):
(WI.Table.prototype._indexToInsertColumn):
(WI.Table.prototype._handleScroll):
(WI.Table.prototype._handleKeyDown):
(WI.Table.prototype._handleClick):
(WI.Table.prototype._handleContextMenu):
(WI.Table.prototype._handleHeaderCellClicked):
(WI.Table.prototype._handleHeaderContextMenu):
Table is mostly a re-implementation of DataGrid. Much of its functionality
was a direct copy that was then modified and simplified for a smaller
and simpler feature set.

Table behaves more like Cocoa's NSTableView. A datasource supplies the
number of rows, which Table uses to resize appropriately. A delegate is
then called to populate cells in a row when they become visible. Table
does minimal caching, and in the event of data source changes
(resorting, adding rows, modifying rows, etc) the visible rows are
simply recreated from scratch. Clients should therefore make generating
a cell's contents as simple and performant as possible.

Unlike DataGrid, rows are just an <li> with a bunch of cells. Since the
number of rows are limited to (roughly) those that are visible, most
operations, like resizing, creates / modifies each of the visible cells.

Finally, Table's resizing operations behave more like flexible content
than DataGrid's neighbor only approach. This makes resizing the table
generally easier to do, but may need refinement to decide which columns
we would prefer to distribute columns during resizing.

* UserInterface/Views/Table.css: Added.
(.table):
(.table > .header):
(.table > .header > .sortable:active):
(.table > .header > :matches(.sort-ascending, .sort-descending)):
(.table > .header > :matches(.sort-ascending, .sort-descending)::after):
(body[dir=ltr] .table > .header > :matches(.sort-ascending, .sort-descending)):
(body[dir=rtl] .table > .header > :matches(.sort-ascending, .sort-descending)):
(body[dir=ltr] .table > .header > :matches(.sort-ascending, .sort-descending)::after):
(body[dir=rtl] .table > .header > :matches(.sort-ascending, .sort-descending)::after):
(.table > .header > .sort-ascending::after):
(.table > .header > .sort-descending::after):
(.table > .data-container):
(.table > .data-container.not-scrollable):
(.table > .data-container > .data-list):
(.table > .data-container > .data-list.odd-first-zebra-stripe):
(.table > .data-container > .data-list > li):
(.table > .data-container > .data-list > li.selected):
(.table:focus > .data-container > .data-list li.selected):
(.table .cell):
(body[dir=ltr] .table .cell:not(:last-child)):
(body[dir=rtl] .table .cell:not(:last-child)):
(body[dir=ltr] .table .cell:first-child):
(body[dir=rtl] .table .cell:first-child):
(.table :not(.header) .cell:first-of-type):
(.table .cell.align-right):
(.table .cell.align-left):
(.table .cell.align-center):
Styles mostly taken from DataGrid with a few minor tweaks.

* UserInterface/Views/NetworkTableContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/NetworkGridContentView.css.
(.content-view.network .table .icon):
(body[dir=ltr] .content-view.network .table .icon):
(body[dir=rtl] .content-view.network .table .icon):
(.content-view.network .table li:not(.filler) .cell.name):
(.content-view.network .table .cache-type):
(.content-view.network .table .error):
* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView):
(WI.NetworkTableContentView.shortDisplayNameForResourceType):
(WI.NetworkTableContentView.prototype.get navigationItems):
(WI.NetworkTableContentView.prototype.shown):
(WI.NetworkTableContentView.prototype.closed):
(WI.NetworkTableContentView.prototype.reset):
(WI.NetworkTableContentView.prototype.tableNumberOfRows):
(WI.NetworkTableContentView.prototype.tableSortChanged):
(WI.NetworkTableContentView.prototype.tableCellClicked):
(WI.NetworkTableContentView.prototype.tableCellContextMenuClicked):
(WI.NetworkTableContentView.prototype.tableSelectedRowChanged):
(WI.NetworkTableContentView.prototype.tablePopulateCell):
(WI.NetworkTableContentView.prototype._populateNameCell):
(WI.NetworkTableContentView.prototype._populateTransferSizeCell):
(WI.NetworkTableContentView.prototype._generateSortComparator):
(WI.NetworkTableContentView.prototype.initialLayout):
(WI.NetworkTableContentView.prototype.layout):
(WI.NetworkTableContentView.prototype._processPendingEntries):
(WI.NetworkTableContentView.prototype._rowIndexForResource):
(WI.NetworkTableContentView.prototype._updateEntryForResource):
(WI.NetworkTableContentView.prototype._mainResourceDidChange):
(WI.NetworkTableContentView.prototype._resourceLoadingDidFinish):
(WI.NetworkTableContentView.prototype._resourceLoadingDidFail):
(WI.NetworkTableContentView.prototype._resourceTransferSizeDidChange):
(WI.NetworkTableContentView.prototype._networkTimelineRecordAdded):
(WI.NetworkTableContentView.prototype._isDefaultSort):
(WI.NetworkTableContentView.prototype._insertResourceAndReloadTable):
(WI.NetworkTableContentView.prototype._displayType):
(WI.NetworkTableContentView.prototype._entryForResource):
(WI.NetworkTableContentView.prototype._passFilter):
(WI.NetworkTableContentView.prototype._updateSortAndFilteredEntries):
(WI.NetworkTableContentView.prototype._updateFilteredEntries):
(WI.NetworkTableContentView.prototype._generateTypeFilter):
(WI.NetworkTableContentView.prototype._areFilterListsIdentical):
(WI.NetworkTableContentView.prototype._typeFilterScopeBarSelectionChanged):
(WI.NetworkTableContentView.prototype._tableNameColumnDidChangeWidth):
The NetworkTableContentView has a Table and it is the table's data source
and delegate. It contains a complete list of entries, and a filtered list
of entries, which is the backing list of rows for the table. As entries
are created, updated, or filters modified, this generally modifies
entries, sorts, filters, and reloads the table. As much as possible it
batches operations in the usual layout loop.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@222470 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Views/NetworkGridContentView.css
Source/WebInspectorUI/UserInterface/Views/NetworkTabContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkTabContentView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Table.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Table.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/TableColumn.js [new file with mode: 0644]

index 25f4c28..7d1d4be 100644 (file)
@@ -1,3 +1,209 @@
+2017-09-22  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Include a table in New Network Tab
+        https://bugs.webkit.org/show_bug.cgi?id=177206
+
+        Reviewed by Matt Baker and Brian Burg.
+
+        This includes an initial implementation of the NetworkTableContentView,
+        and a generic Table / TableColumn implementation ported from DataGrid.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New strings and files.
+
+        * UserInterface/Views/NetworkGridContentView.css:
+        (.content-view:matches(.network, .network-grid) > .data-grid .cache-type):
+        (.cache-type): Deleted.
+        * UserInterface/Views/ResourceDetailsSidebarPanel.css:
+        (.sidebar > .panel.resource-details .cache-type):
+        Make the .cache-type selector more specific for the legacy cases.
+
+        * UserInterface/Views/NetworkTabContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/NetworkGridContentView.css.
+        (.content-view.network > .content-browser):
+        * UserInterface/Views/NetworkTabContentView.js:
+        (WI.NetworkTabContentView.prototype.shown):
+        (WI.NetworkTabContentView.prototype.hidden):
+        (WI.NetworkTabContentView.prototype.closed):
+        NetworkTabContentView has a ContentBrowser so it should be passing
+        ContentBrowser lifecycle events (shown, hidden, closed) through to
+        the sub-content browser.
+
+        * UserInterface/Views/TableColumn.js: Added.
+        (WI.TableColumn.prototype.get identifier):
+        (WI.TableColumn.prototype.get name):
+        (WI.TableColumn.prototype.get width):
+        (WI.TableColumn.prototype.get minWidth):
+        (WI.TableColumn.prototype.get maxWidth):
+        (WI.TableColumn.prototype.get hidden):
+        (WI.TableColumn.prototype.get defaultHidden):
+        (WI.TableColumn.prototype.get sortable):
+        (WI.TableColumn.prototype.get align):
+        (WI.TableColumn.prototype.get locked):
+        (WI.TableColumn.prototype.get flexible):
+        (WI.TableColumn.prototype.setWidth):
+        (WI.TableColumn.prototype.setHidden):
+        Model object for a column. Values are getter only. Columns may express
+        size constraints (min width / max width) that are respected as much as
+        possible. When a column is resized it dispatches an event.
+
+        * UserInterface/Views/Table.js: Added.
+        (WI.Table):
+        (WI.Table.prototype.get element):
+        (WI.Table.prototype.get identifier):
+        (WI.Table.prototype.get dataSource):
+        (WI.Table.prototype.get delegate):
+        (WI.Table.prototype.get rowHeight):
+        (WI.Table.prototype.get selectedRow):
+        (WI.Table.prototype.get sortOrder):
+        (WI.Table.prototype.set sortOrder):
+        (WI.Table.prototype.get sortColumnIdentifier):
+        (WI.Table.prototype.set sortColumnIdentifier):
+        (WI.Table.prototype.resize):
+        (WI.Table.prototype.reloadData):
+        (WI.Table.prototype.reloadDataAddedToEndOnly):
+        (WI.Table.prototype.reloadRow):
+        (WI.Table.prototype.reloadCell):
+        (WI.Table.prototype.selectRow):
+        (WI.Table.prototype.clearSelectedRow):
+        (WI.Table.prototype.columnWithIdentifier):
+        (WI.Table.prototype.addColumn):
+        (WI.Table.prototype.showColumn):
+        (WI.Table.prototype.hideColumn):
+        (WI.Table.prototype.restoreScrollPosition):
+        (WI.Table.prototype.initialLayout):
+        (WI.Table.prototype.layout):
+        (WI.Table.prototype.resizerDragStarted):
+        (WI.Table.prototype.resizerDragging.growableSize):
+        (WI.Table.prototype.resizerDragging.shrinkableSize):
+        (WI.Table.prototype.resizerDragging.canGrow):
+        (WI.Table.prototype.resizerDragging.canShrink):
+        (WI.Table.prototype.resizerDragging.columnToResize):
+        (WI.Table.prototype.resizerDragging):
+        (WI.Table.prototype.resizerDragEnded):
+        (WI.Table.prototype._needsLayout):
+        (WI.Table.prototype._createHeaderCell):
+        (WI.Table.prototype._createFillerCell):
+        (WI.Table.prototype._createCell):
+        (WI.Table.prototype._getOrCreateRow):
+        (WI.Table.prototype._populatedCellForColumnAndRow):
+        (WI.Table.prototype._populateRow):
+        (WI.Table.prototype._resizeColumnsAndFiller.distributeRemainingPixels):
+        (WI.Table.prototype._resizeColumnsAndFiller.bestFit):
+        (WI.Table.prototype._resizeColumnsAndFiller):
+        (WI.Table.prototype._updateVisibleRows):
+        (WI.Table.prototype._applyColumnWidths):
+        (WI.Table.prototype._positionResizerElements):
+        (WI.Table.prototype._isRowVisible):
+        (WI.Table.prototype._indexToInsertColumn):
+        (WI.Table.prototype._handleScroll):
+        (WI.Table.prototype._handleKeyDown):
+        (WI.Table.prototype._handleClick):
+        (WI.Table.prototype._handleContextMenu):
+        (WI.Table.prototype._handleHeaderCellClicked):
+        (WI.Table.prototype._handleHeaderContextMenu):
+        Table is mostly a re-implementation of DataGrid. Much of its functionality
+        was a direct copy that was then modified and simplified for a smaller
+        and simpler feature set.
+        
+        Table behaves more like Cocoa's NSTableView. A datasource supplies the
+        number of rows, which Table uses to resize appropriately. A delegate is
+        then called to populate cells in a row when they become visible. Table
+        does minimal caching, and in the event of data source changes
+        (resorting, adding rows, modifying rows, etc) the visible rows are
+        simply recreated from scratch. Clients should therefore make generating
+        a cell's contents as simple and performant as possible.
+
+        Unlike DataGrid, rows are just an <li> with a bunch of cells. Since the
+        number of rows are limited to (roughly) those that are visible, most
+        operations, like resizing, creates / modifies each of the visible cells.
+
+        Finally, Table's resizing operations behave more like flexible content
+        than DataGrid's neighbor only approach. This makes resizing the table
+        generally easier to do, but may need refinement to decide which columns
+        we would prefer to distribute columns during resizing.
+
+        * UserInterface/Views/Table.css: Added.
+        (.table):
+        (.table > .header):
+        (.table > .header > .sortable:active):
+        (.table > .header > :matches(.sort-ascending, .sort-descending)):
+        (.table > .header > :matches(.sort-ascending, .sort-descending)::after):
+        (body[dir=ltr] .table > .header > :matches(.sort-ascending, .sort-descending)):
+        (body[dir=rtl] .table > .header > :matches(.sort-ascending, .sort-descending)):
+        (body[dir=ltr] .table > .header > :matches(.sort-ascending, .sort-descending)::after):
+        (body[dir=rtl] .table > .header > :matches(.sort-ascending, .sort-descending)::after):
+        (.table > .header > .sort-ascending::after):
+        (.table > .header > .sort-descending::after):
+        (.table > .data-container):
+        (.table > .data-container.not-scrollable):
+        (.table > .data-container > .data-list):
+        (.table > .data-container > .data-list.odd-first-zebra-stripe):
+        (.table > .data-container > .data-list > li):
+        (.table > .data-container > .data-list > li.selected):
+        (.table:focus > .data-container > .data-list li.selected):
+        (.table .cell):
+        (body[dir=ltr] .table .cell:not(:last-child)):
+        (body[dir=rtl] .table .cell:not(:last-child)):
+        (body[dir=ltr] .table .cell:first-child):
+        (body[dir=rtl] .table .cell:first-child):
+        (.table :not(.header) .cell:first-of-type):
+        (.table .cell.align-right):
+        (.table .cell.align-left):
+        (.table .cell.align-center):
+        Styles mostly taken from DataGrid with a few minor tweaks.
+
+        * UserInterface/Views/NetworkTableContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/NetworkGridContentView.css.
+        (.content-view.network .table .icon):
+        (body[dir=ltr] .content-view.network .table .icon):
+        (body[dir=rtl] .content-view.network .table .icon):
+        (.content-view.network .table li:not(.filler) .cell.name):
+        (.content-view.network .table .cache-type):
+        (.content-view.network .table .error):
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView):
+        (WI.NetworkTableContentView.shortDisplayNameForResourceType):
+        (WI.NetworkTableContentView.prototype.get navigationItems):
+        (WI.NetworkTableContentView.prototype.shown):
+        (WI.NetworkTableContentView.prototype.closed):
+        (WI.NetworkTableContentView.prototype.reset):
+        (WI.NetworkTableContentView.prototype.tableNumberOfRows):
+        (WI.NetworkTableContentView.prototype.tableSortChanged):
+        (WI.NetworkTableContentView.prototype.tableCellClicked):
+        (WI.NetworkTableContentView.prototype.tableCellContextMenuClicked):
+        (WI.NetworkTableContentView.prototype.tableSelectedRowChanged):
+        (WI.NetworkTableContentView.prototype.tablePopulateCell):
+        (WI.NetworkTableContentView.prototype._populateNameCell):
+        (WI.NetworkTableContentView.prototype._populateTransferSizeCell):
+        (WI.NetworkTableContentView.prototype._generateSortComparator):
+        (WI.NetworkTableContentView.prototype.initialLayout):
+        (WI.NetworkTableContentView.prototype.layout):
+        (WI.NetworkTableContentView.prototype._processPendingEntries):
+        (WI.NetworkTableContentView.prototype._rowIndexForResource):
+        (WI.NetworkTableContentView.prototype._updateEntryForResource):
+        (WI.NetworkTableContentView.prototype._mainResourceDidChange):
+        (WI.NetworkTableContentView.prototype._resourceLoadingDidFinish):
+        (WI.NetworkTableContentView.prototype._resourceLoadingDidFail):
+        (WI.NetworkTableContentView.prototype._resourceTransferSizeDidChange):
+        (WI.NetworkTableContentView.prototype._networkTimelineRecordAdded):
+        (WI.NetworkTableContentView.prototype._isDefaultSort):
+        (WI.NetworkTableContentView.prototype._insertResourceAndReloadTable):
+        (WI.NetworkTableContentView.prototype._displayType):
+        (WI.NetworkTableContentView.prototype._entryForResource):
+        (WI.NetworkTableContentView.prototype._passFilter):
+        (WI.NetworkTableContentView.prototype._updateSortAndFilteredEntries):
+        (WI.NetworkTableContentView.prototype._updateFilteredEntries):
+        (WI.NetworkTableContentView.prototype._generateTypeFilter):
+        (WI.NetworkTableContentView.prototype._areFilterListsIdentical):
+        (WI.NetworkTableContentView.prototype._typeFilterScopeBarSelectionChanged):
+        (WI.NetworkTableContentView.prototype._tableNameColumnDidChangeWidth):
+        The NetworkTableContentView has a Table and it is the table's data source
+        and delegate. It contains a complete list of entries, and a filtered list
+        of entries, which is the backing list of rows for the table. As entries
+        are created, updated, or filters modified, this generally modifies
+        entries, sorts, filters, and reloads the table. As much as possible it
+        batches operations in the usual layout loop.
+
 2017-09-24  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: Reduce work during resizing
index 32d1d97..7e81d91 100644 (file)
@@ -50,7 +50,9 @@ localizedStrings["(Memory)"] = "(Memory)";
 localizedStrings["(Tail Call)"] = "(Tail Call)";
 localizedStrings["(anonymous function)"] = "(anonymous function)";
 localizedStrings["(async)"] = "(async)";
+localizedStrings["(disk)"] = "(disk)";
 localizedStrings["(many)"] = "(many)";
+localizedStrings["(memory)"] = "(memory)";
 localizedStrings["(modify the boxes below to add a value)"] = "(modify the boxes below to add a value)";
 localizedStrings["(multiple)"] = "(multiple)";
 localizedStrings["(program)"] = "(program)";
@@ -226,10 +228,8 @@ localizedStrings["Console Profile Recorded"] = "Console Profile Recorded";
 localizedStrings["Console cleared at %s"] = "Console cleared at %s";
 localizedStrings["Console opened at %s"] = "Console opened at %s";
 localizedStrings["Console:"] = "Console:";
-localizedStrings["Container Regions"] = "Container Regions";
 localizedStrings["Containing"] = "Containing";
 localizedStrings["Content"] = "Content";
-localizedStrings["Content Flow"] = "Content Flow";
 localizedStrings["Content Security Policy violation of directive: %s"] = "Content Security Policy violation of directive: %s";
 localizedStrings["Continuation Frame"] = "Continuation Frame";
 localizedStrings["Continue script execution (%s or %s)"] = "Continue script execution (%s or %s)";
@@ -690,7 +690,6 @@ localizedStrings["Reference Issue"] = "Reference Issue";
 localizedStrings["Reflection"] = "Reflection";
 localizedStrings["Refresh"] = "Refresh";
 localizedStrings["Refresh watch expressions"] = "Refresh watch expressions";
-localizedStrings["Region Flow"] = "Region Flow";
 localizedStrings["Region announced in its entirety."] = "Region announced in its entirety.";
 localizedStrings["Regular Expression"] = "Regular Expression";
 localizedStrings["Reload Web Inspector"] = "Reload Web Inspector";
@@ -715,6 +714,7 @@ localizedStrings["Requesting: %s"] = "Requesting: %s";
 localizedStrings["Required"] = "Required";
 localizedStrings["Reset"] = "Reset";
 localizedStrings["Resource"] = "Resource";
+localizedStrings["Resource Size"] = "Resource Size";
 localizedStrings["Resource Type"] = "Resource Type";
 localizedStrings["Resource failed to load."] = "Resource failed to load.";
 localizedStrings["Resource was loaded with the “data“ scheme."] = "Resource was loaded with the “data“ scheme.";
@@ -907,6 +907,7 @@ localizedStrings["Total memory size at the end of the selected time range"] = "T
 localizedStrings["Total time"] = "Total time";
 localizedStrings["Trace"] = "Trace";
 localizedStrings["Trace: %s"] = "Trace: %s";
+localizedStrings["Transfer Size"] = "Transfer Size";
 localizedStrings["Transferred"] = "Transferred";
 localizedStrings["Transform"] = "Transform";
 localizedStrings["Transition"] = "Transition";
@@ -943,11 +944,12 @@ localizedStrings["Vertex"] = "Vertex";
 localizedStrings["Vertex Shader"] = "Vertex Shader";
 localizedStrings["Vertical"] = "Vertical";
 localizedStrings["View variable value"] = "View variable value";
-localizedStrings["Visible"] = "Visible";
 localizedStrings["Visibility"] = "Visibility";
+localizedStrings["Visible"] = "Visible";
 localizedStrings["Warning: "] = "Warning: ";
 localizedStrings["Warnings"] = "Warnings";
 localizedStrings["Watch Expressions"] = "Watch Expressions";
+localizedStrings["Waterfall"] = "Waterfall";
 localizedStrings["Web Inspector"] = "Web Inspector";
 localizedStrings["WebSocket Connection Established"] = "WebSocket Connection Established";
 localizedStrings["Weight"] = "Weight";
index 12a295b..05c9989 100644 (file)
     <link rel="stylesheet" href="Views/NavigationBar.css">
     <link rel="stylesheet" href="Views/NavigationSidebarPanel.css">
     <link rel="stylesheet" href="Views/NetworkGridContentView.css">
+    <link rel="stylesheet" href="Views/NetworkTabContentView.css">
+    <link rel="stylesheet" href="Views/NetworkTableContentView.css">
     <link rel="stylesheet" href="Views/NetworkTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/NetworkTimelineView.css">
     <link rel="stylesheet" href="Views/NewTabContentView.css">
     <link rel="stylesheet" href="Views/RenderingFrameTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/RenderingFrameTimelineView.css">
     <link rel="stylesheet" href="Views/Resizer.css">
+    <link rel="stylesheet" href="Views/ResourceDetailsSidebarPanel.css">
+    <link rel="stylesheet" href="Views/ResourceHeadersContentView.css">
     <link rel="stylesheet" href="Views/ResourceIcons.css">
     <link rel="stylesheet" href="Views/ResourceSidebarPanel.css">
     <link rel="stylesheet" href="Views/ResourceTimelineDataGridNode.css">
     <link rel="stylesheet" href="Views/SyntaxHighlightingDefaultTheme.css">
     <link rel="stylesheet" href="Views/TabBar.css">
     <link rel="stylesheet" href="Views/TabBrowser.css">
+    <link rel="stylesheet" href="Views/Table.css">
     <link rel="stylesheet" href="Views/TextContentView.css">
     <link rel="stylesheet" href="Views/TextEditor.css">
     <link rel="stylesheet" href="Views/TextNavigationItem.css">
     <script src="Views/TabBar.js"></script>
     <script src="Views/TabBarItem.js"></script>
     <script src="Views/TabBrowser.js"></script>
+    <script src="Views/Table.js"></script>
+    <script src="Views/TableColumn.js"></script>
     <script src="Views/TextEditor.js"></script>
     <script src="Views/TimelineOverviewGraph.js"></script>
     <script src="Views/TimelineView.js"></script>
index 0bdb933..ee09c88 100644 (file)
@@ -43,7 +43,7 @@
     filter: grayscale();
 }
 
-.cache-type {
+.content-view:matches(.network, .network-grid) > .data-grid .cache-type {
     color: gray;
 }
 
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkTabContentView.css b/Source/WebInspectorUI/UserInterface/Views/NetworkTabContentView.css
new file mode 100644 (file)
index 0000000..6a7d10f
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.network > .content-browser {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+}
index fcb9a44..0b4de58 100644 (file)
@@ -57,6 +57,29 @@ WI.NetworkTabContentView = class NetworkTabContentView extends WI.TabContentView
         return !!window.NetworkAgent && !!window.PageAgent && WI.settings.experimentalEnableNewNetworkTab.value;
     }
 
+    // Protected
+
+    shown()
+    {
+        super.shown();
+
+        this._contentBrowser.shown();
+    }
+
+    hidden()
+    {
+        this._contentBrowser.hidden();
+
+        super.hidden();
+    }
+
+    closed()
+    {
+        this._contentBrowser.contentViewContainer.closeAllContentViews();
+
+        super.closed();
+    }
+
     // Public
 
     get contentBrowser() { return this._contentBrowser; }
diff --git a/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.css b/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.css
new file mode 100644 (file)
index 0000000..9eb17cd
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.network .table .icon {
+    position: relative;
+    width: 16px;
+    height: 16px;
+    bottom: 1px;
+    vertical-align: middle;
+    -webkit-margin-end: 4px;
+}
+
+.content-view.network .table li:not(.filler) .cell.name {
+    cursor: pointer;
+}
+
+.content-view.network .table .cache-type {
+    color: var(--text-color-gray-medium);
+}
+
+.content-view.network .table .error {
+    color: var(--error-text-color);
+}
index 8f611db..f2b1701 100644 (file)
@@ -29,13 +29,46 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         super(representedObject);
 
-        // FIXME: Network Table.
+        this._entries = [];
+        this._entriesSortComparator = null;
+        this._filteredEntries = [];
+        this._pendingInsertions = [];
+        this._pendingUpdates = [];
+
+        this._table = null;
+        this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
+
+        // FIXME: Resource Detail View.
         // FIXME: Network Timeline.
         // FIXME: Filter text field.
-        // FIXME: Filter scope bar.
         // FIXME: Throttling.
         // FIXME: HAR Export.
 
+        const exclusive = true;
+        this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), exclusive);
+        let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
+
+        let uniqueTypes = [
+            ["Document", (type) => type === WI.Resource.Type.Document],
+            ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
+            ["Image", (type) => type === WI.Resource.Type.Image],
+            ["Font", (type) => type === WI.Resource.Type.Font],
+            ["Script", (type) => type === WI.Resource.Type.Script],
+            ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
+            ["Other", (type) => type === WI.Resource.Type.Other || type === WI.Resource.Type.WebSocket],
+        ];
+        for (let [key, checker] of uniqueTypes) {
+            let type = WI.Resource.Type[key];
+            let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type))
+            scopeBarItem.__checker = checker;
+            typeFilterScopeBarItems.push(scopeBarItem);
+        }
+
+        this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
+        this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
+
+        this._activeTypeFilters = this._generateTypeFilter();
+
         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
@@ -49,6 +82,39 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemClear.svg", 16, 16);
         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => this.reset());
+
+        WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
+        WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
+        WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
+        WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
+        WI.timelineManager.persistentNetworkTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
+    }
+
+    // Static
+
+    static shortDisplayNameForResourceType(type)
+    {
+        switch (type) {
+        case WI.Resource.Type.Document:
+            return WI.UIString("Document");
+        case WI.Resource.Type.Stylesheet:
+            return "CSS";
+        case WI.Resource.Type.Image:
+            return WI.UIString("Image");
+        case WI.Resource.Type.Font:
+            return WI.UIString("Font");
+        case WI.Resource.Type.Script:
+            return "JS";
+        case WI.Resource.Type.XHR:
+        case WI.Resource.Type.Fetch:
+            return "XHR";
+        case WI.Resource.Type.WebSocket:
+        case WI.Resource.Type.Other:
+            return WI.UIString("Other");
+        default:
+            console.error("Unknown resource type", type);
+            return null;
+        }
     }
 
     // Public
@@ -60,7 +126,7 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     get navigationItems()
     {
-        let items = [];
+        let items = [this._typeFilterScopeBar];
 
         if (this._disableResourceCacheNavigationItem)
             items.push(this._disableResourceCacheNavigationItem);
@@ -73,33 +139,376 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         super.shown();
 
-        // FIXME: Implement.
+        if (this._table)
+            this._table.restoreScrollPosition();
     }
 
-    hidden()
+    closed()
     {
-        // FIXME: Implment.
+        super.closed();
 
-        super.hidden();
+        WI.Frame.removeEventListener(null, null, this);
+        WI.Resource.removeEventListener(null, null, this);
+        WI.timelineManager.persistentNetworkTimeline.removeEventListener(WI.Timeline.Event.RecordAdded, this._networkTimelineRecordAdded, this);
     }
 
-    closed()
+    reset()
     {
-        // FIXME: Implement
+        this._entries = [];
+        this._filteredEntries = [];
+        this._pendingInsertions = [];
 
-        super.closed();
+        if (this._table) {
+            this._table.clearSelectedRow();
+            this._table.reloadData();
+        }
     }
 
-    reset()
+    // Table dataSource
+
+    tableNumberOfRows(table)
+    {
+        return this._filteredEntries.length;
+    }
+
+    tableSortChanged(table)
+    {
+        this._generateSortComparator();
+
+        if (!this._entriesSortComparator)
+            return;
+
+        this._entries = this._entries.sort(this._entriesSortComparator);
+        this._updateFilteredEntries();
+        this._table.reloadData();
+    }
+
+    // Table delegate
+
+    tableCellClicked(table, cell, column, rowIndex, event)
+    {
+        // FIXME: Show resource detail view.
+    }
+
+    tableCellContextMenuClicked(table, cell, column, rowIndex, event)
+    {
+        if (column !== this._nameColumn)
+            return;
+
+        this._table.selectRow(rowIndex);
+
+        let entry = this._filteredEntries[rowIndex];
+        let contextMenu = WI.ContextMenu.createFromEvent(event);
+        WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
+    }
+
+    tableSelectedRowChanged(table, rowIndex)
+    {
+        // FIXME: Show resource detail view.
+    }
+
+    tablePopulateCell(table, cell, column, rowIndex)
+    {
+        let entry = this._filteredEntries[rowIndex];
+
+        cell.classList.toggle("error", entry.resource.hadLoadingError());
+
+        switch (column.identifier) {
+        case "name":
+            this._populateNameCell(cell, entry);
+            break;
+        case "domain":
+            cell.textContent = entry.domain || emDash;
+            break;
+        case "type":
+            cell.textContent = entry.displayType || emDash;
+            break;
+        case "mimeType":
+            cell.textContent = entry.mimeType || emDash;
+            break;
+        case "method":
+            cell.textContent = entry.method || emDash;
+            break;
+        case "scheme":
+            cell.textContent = entry.scheme || emDash;
+            break;
+        case "status":
+            cell.textContent = entry.status || emDash;
+            break;
+        case "protocol":
+            cell.textContent = entry.protocol || emDash;
+            break;
+        case "priority":
+            cell.textContent = WI.Resource.displayNameForPriority(entry.priority) || emDash;
+            break;
+        case "remoteAddress":
+            cell.textContent = entry.remoteAddress || emDash;
+            break;
+        case "connectionIdentifier":
+            cell.textContent = entry.connectionIdentifier || emDash;
+            break;
+        case "resourceSize":
+            cell.textContent = isNaN(entry.resourceSize) ? emDash : Number.bytesToString(entry.resourceSize);
+            break;
+        case "transferSize":
+            this._populateTransferSizeCell(cell, entry);
+            break;
+        case "time":
+            // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
+            cell.textContent = isNaN(entry.time) ? emDash : Number.secondsToString(Math.max(entry.time, 0));
+            break;
+        case "waterfall":
+            // FIXME: Waterfall graph.
+            cell.textContent = emDash;
+            break;
+        }
+
+        return cell;
+    }
+
+    // Private
+
+    _populateNameCell(cell, entry)
+    {
+        cell.removeChildren();
+
+        let iconElement = cell.appendChild(document.createElement("img"));
+        iconElement.className = "icon";
+        cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, entry.resource.type);
+
+        let nameElement = cell.appendChild(document.createElement("span"));
+        nameElement.textContent = entry.name;
+    }
+
+    _populateTransferSizeCell(cell, entry)
+    {
+        let responseSource = entry.resource.responseSource;
+        if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
+            cell.classList.add("cache-type");
+            cell.textContent = WI.UIString("(memory)");
+            return;
+        }
+        if (responseSource === WI.Resource.ResponseSource.DiskCache) {
+            cell.classList.add("cache-type");
+            cell.textContent = WI.UIString("(disk)");
+            return;
+        }
+
+        let transferSize = entry.transferSize;
+        cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
+        console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
+    }
+
+    _generateSortComparator()
     {
-        // FIXME: Implement
+        let sortColumnIdentifier = this._table.sortColumnIdentifier;
+        if (!sortColumnIdentifier) {
+            this._entriesSortComparator = null;
+            return;
+        }
+
+        let comparator;
+
+        switch (sortColumnIdentifier) {
+        case "name":
+        case "domain":
+        case "mimeType":
+        case "method":
+        case "scheme":
+        case "protocol":
+        case "remoteAddress":
+            // Simple string.
+            comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
+            break;
+
+        case "status":
+        case "connectionIdentifier":
+        case "resourceSize":
+        case "time":
+            // Simple number.
+            comparator = (a, b) => {
+                let aValue = a[sortColumnIdentifier];
+                if (isNaN(aValue))
+                    return 1;
+                let bValue = b[sortColumnIdentifier];
+                if (isNaN(bValue))
+                    return -1;
+                return aValue - bValue;
+            }
+            break;
+
+        case "priority":
+            // Resource.NetworkPriority enum.
+            comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
+            break;
+
+        case "type":
+            // Sort by displayType string.
+            comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
+            break;
+
+        case "transferSize":
+            // Handle (memory) and (disk) values.
+            comparator = (a, b) => {
+                let transferSizeA = a.transferSize;
+                let transferSizeB = b.transferSize;
+
+                // Treat NaN as the largest value.
+                if (isNaN(transferSizeA))
+                    return 1;
+                if (isNaN(transferSizeB))
+                    return -1;
+
+                // Treat memory cache and disk cache as small values.
+                let sourceA = a.resource.responseSource;
+                if (sourceA === WI.Resource.ResponseSource.MemoryCache)
+                    transferSizeA = -20;
+                else if (sourceA === WI.Resource.ResponseSource.DiskCache)
+                    transferSizeA = -10;
+
+                let sourceB = b.resource.responseSource;
+                if (sourceB === WI.Resource.ResponseSource.MemoryCache)
+                    transferSizeB = -20;
+                else if (sourceB === WI.Resource.ResponseSource.DiskCache)
+                    transferSizeB = -10;
+
+                return transferSizeA - transferSizeB;
+            };
+            break;
+
+        case "waterfall":
+            // Sort by startTime number.
+            comparator = comparator = (a, b) => a.startTime - b.startTime;
+            break;
+
+        default:
+            console.assert("Unexpected sort column", sortColumnIdentifier);
+            return;
+        }
+
+        let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
+        this._entriesSortComparator = (a, b) => reverseFactor * comparator(a, b);
     }
 
     // Protected
 
+    initialLayout()
+    {
+        this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
+            initialWidth: this._nameColumnWidthSetting.value,
+            minWidth: WI.Sidebar.AbsoluteMinimumWidth,
+            maxWidth: 500,
+            resizeType: WI.TableColumn.ResizeType.Locked,
+        });
+
+        this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
+
+        this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
+            minWidth: 120,
+            maxWidth: 200,
+        });
+
+        this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
+            minWidth: 70,
+            maxWidth: 120,
+        });
+
+        this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
+            hidden: true,
+            minWidth: 100,
+            maxWidth: 150,
+        });
+
+        this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
+            hidden: true,
+            minWidth: 55,
+            maxWidth: 80,
+        });
+
+        this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
+            hidden: true,
+            minWidth: 55,
+            maxWidth: 80,
+        });
+
+        this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
+            hidden: true,
+            minWidth: 50,
+            maxWidth: 50,
+            align: "left",
+        });
+
+        this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
+            hidden: true,
+            minWidth: 65,
+            maxWidth: 80,
+        });
+
+        this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
+            hidden: true,
+            minWidth: 65,
+            maxWidth: 80,
+        });
+
+        this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
+            hidden: true,
+            minWidth: 150,
+        });
+
+        this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
+            hidden: true,
+            minWidth: 50,
+            maxWidth: 120,
+            align: "right",
+        });
+
+        this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
+            hidden: true,
+            minWidth: 80,
+            maxWidth: 100,
+            align: "right",
+        });
+
+        this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
+            minWidth: 100,
+            maxWidth: 150,
+            align: "right",
+        });
+
+        this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
+            minWidth: 65,
+            maxWidth: 90,
+            align: "right",
+        });
+
+        this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
+            minWidth: 230,
+        });
+
+        this._table = new WI.Table("network-table", this, this, 20);
+
+        this._table.addColumn(this._nameColumn);
+        this._table.addColumn(this._domainColumn);
+        this._table.addColumn(this._typeColumn);
+        this._table.addColumn(this._mimeTypeColumn);
+        this._table.addColumn(this._methodColumn);
+        this._table.addColumn(this._schemeColumn);
+        this._table.addColumn(this._statusColumn);
+        this._table.addColumn(this._protocolColumn);
+        this._table.addColumn(this._priorityColumn);
+        this._table.addColumn(this._remoteAddressColumn);
+        this._table.addColumn(this._connectionIdentifierColumn);
+        this._table.addColumn(this._resourceSizeColumn);
+        this._table.addColumn(this._transferSizeColumn);
+        this._table.addColumn(this._timeColumn);
+        this._table.addColumn(this._waterfallColumn);
+
+        this.addSubview(this._table);
+    }
+
     layout()
     {
-        // FIXME: Implement
+        this._processPendingEntries();
     }
 
     handleClearShortcut(event)
@@ -109,6 +518,53 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
 
     // Private
 
+    _processPendingEntries()
+    {
+        let needsSort = this._pendingUpdates.length > 0;
+
+        // No global sort is needed, so just insert new records into their sorted position.
+        if (!needsSort) {
+            let originalLength = this._pendingInsertions.length;
+            for (let resource of this._pendingInsertions)
+                this._insertResourceAndReloadTable(resource);
+            console.assert(this._pendingInsertions.length === originalLength);
+            this._pendingInsertions = [];
+            return;
+        }
+
+        for (let resource of this._pendingInsertions)
+            this._entries.push(this._entryForResource(resource));
+        this._pendingInsertions = [];
+
+        for (let resource of this._pendingUpdates)
+            this._updateEntryForResource(resource);
+        this._pendingUpdates = [];
+
+        this._updateSortAndFilteredEntries();
+        this._table.reloadData();
+    }
+
+    _rowIndexForResource(resource)
+    {
+        return this._filteredEntries.findIndex((x) => x.resource === resource);
+    }
+
+    _updateEntryForResource(resource)
+    {
+        let index = this._entries.findIndex((x) => x.resource === resource);
+        if (index === -1)
+            return;
+
+        let entry = this._entryForResource(resource);
+        this._entries[index] = entry;
+
+        let rowIndex = this._rowIndexForResource(resource);
+        if (rowIndex === -1)
+            return;
+
+        this._filteredEntries[rowIndex] = entry;
+    }
+
     _resourceCachingDisabledSettingChanged()
     {
         this._disableResourceCacheNavigationItem.activated = WI.resourceCachingDisabledSetting.value;
@@ -118,4 +574,217 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
     {
         WI.resourceCachingDisabledSetting.value = !WI.resourceCachingDisabledSetting.value;
     }
+
+    _mainResourceDidChange(event)
+    {
+        let frame = event.target;
+        if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
+            return;
+
+        this.reset();
+
+        this._insertResourceAndReloadTable(frame.mainResource);
+    }
+
+    _resourceLoadingDidFinish(event)
+    {
+        let resource = event.target;
+        this._pendingUpdates.push(resource);
+        this.needsLayout();
+    }
+
+    _resourceLoadingDidFail(event)
+    {
+        let resource = event.target;
+        this._pendingUpdates.push(resource);
+        this.needsLayout();
+    }
+
+    _resourceTransferSizeDidChange(event)
+    {
+        if (!this._table)
+            return;
+
+        let resource = event.target;
+
+        // In the unlikely event that this is the sort column, we may need to resort.
+        if (this._table.sortColumnIdentifier === "transferSize") {
+            this._pendingUpdates.push(resource);
+            this.needsLayout();
+            return;
+        }
+
+        let index = this._entries.findIndex((x) => x.resource === resource);
+        if (index === -1)
+            return;
+
+        let entry = this._entries[index];
+        entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
+
+        let rowIndex = this._rowIndexForResource(resource);
+        if (rowIndex === -1)
+            return;
+
+        this._table.reloadCell(rowIndex, "transferSize");
+    }
+
+    _networkTimelineRecordAdded(event)
+    {
+        let resourceTimelineRecord = event.data.record;
+        console.assert(resourceTimelineRecord instanceof WI.ResourceTimelineRecord);
+
+        let resource = resourceTimelineRecord.resource;
+        this._insertResourceAndReloadTable(resource)
+    }
+
+    _isDefaultSort()
+    {
+        return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
+    }
+
+    _insertResourceAndReloadTable(resource)
+    {
+        if (!(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
+            this._pendingInsertions.push(resource);
+            return;
+        }
+
+        console.assert(this._table);
+        if (!this._table)
+            return;
+
+        let entry = this._entryForResource(resource);
+
+        // Default sort has fast path.
+        if (this._isDefaultSort() || !this._entriesSortComparator) {
+            this._entries.push(entry);
+            if (this._passFilter(entry)) {
+                this._filteredEntries.push(entry);
+                this._table.reloadDataAddedToEndOnly();
+            }
+            return;
+        }
+
+        insertObjectIntoSortedArray(entry, this._entries, this._entriesSortComparator);
+
+        if (this._passFilter(entry)) {
+            insertObjectIntoSortedArray(entry, this._filteredEntries, this._entriesSortComparator);
+
+            // Probably a useless optimization here, but if we only added this row to the end
+            // we may avoid recreating all visible rows by saying as such.
+            if (this._filteredEntries.lastValue === entry)
+                this._table.reloadDataAddedToEndOnly();
+            else
+                this._table.reloadData();
+        }
+    }
+
+    _displayType(resource)
+    {
+        if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font) {
+            let fileExtension;
+            if (resource.mimeType)
+                fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
+            if (!fileExtension)
+                fileExtension = WI.fileExtensionForURL(resource.url);
+            if (fileExtension)
+                return fileExtension;
+        }
+
+        return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
+    }
+
+    _entryForResource(resource)
+    {
+        // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
+        // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
+
+        return {
+            resource,
+            name: WI.displayNameForURL(resource.url, resource.urlComponents),
+            domain: WI.displayNameForHost(resource.urlComponents.host),
+            scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
+            method: resource.requestMethod,
+            type: resource.type,
+            displayType: this._displayType(resource),
+            mimeType: resource.mimeType,
+            status: resource.statusCode,
+            cached: resource.cached,
+            resourceSize: resource.size,
+            transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
+            time: resource.duration,
+            protocol: resource.protocol,
+            priority: resource.priority,
+            remoteAddress: resource.remoteAddress,
+            connectionIdentifier: resource.connectionIdentifier,
+            startTime: resource.firstTimestamp,
+        };
+    }
+
+    _passFilter(entry)
+    {
+        if (!this._activeTypeFilters)
+            return true;
+
+        return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
+    }
+
+    _updateSortAndFilteredEntries()
+    {
+        this._entries = this._entries.sort(this._entriesSortComparator);
+        this._updateFilteredEntries();
+    }
+
+    _updateFilteredEntries()
+    {
+        if (this._activeTypeFilters)
+            this._filteredEntries = this._entries.filter(this._passFilter, this);
+        else
+            this._filteredEntries = this._entries.slice();
+    }
+
+    _generateTypeFilter()
+    {
+        let selectedItems = this._typeFilterScopeBar.selectedItems;
+        if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
+            return null;
+
+        return selectedItems.map((item) => item.__checker);
+    }
+
+    _areFilterListsIdentical(listA, listB)
+    {
+        if (listA && listB) {
+            if (listA.length !== listB.length)
+                return false;
+
+            for (let i = 0; i < listA.length; ++i) {
+                if (listA[i] !== listB[i])
+                    return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    _typeFilterScopeBarSelectionChanged(event)
+    {
+        // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
+        // We can't use shallow equals here because the contents are functions.
+        let oldFilter = this._activeTypeFilters;
+        let newFilter = this._generateTypeFilter();
+        if (this._areFilterListsIdentical(oldFilter, newFilter))
+            return;
+
+        this._activeTypeFilters = newFilter;
+        this._updateFilteredEntries();
+        this._table.reloadData();
+    }
+
+    _tableNameColumnDidChangeWidth(event)
+    {
+        this._nameColumnWidthSetting.value = event.target.width;
+    }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/ResourceDetailsSidebarPanel.css
new file mode 100644 (file)
index 0000000..2f823c1
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.sidebar > .panel.resource-details .cache-type {
+    color: var(--text-color-gray-medium);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/Table.css b/Source/WebInspectorUI/UserInterface/Views/Table.css
new file mode 100644 (file)
index 0000000..0bb9a2c
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.table {
+    position: relative;
+    outline: none;
+    width: 100%;
+    height: 100%;
+    background: white;
+
+    --even-zebra-stripe-row-background-color: white;
+    --odd-zebra-stripe-row-background-color: hsl(0, 0%, 95%);
+    --table-column-border-start: 1px solid transparent;
+    --table-column-border-end: 0.5px solid var(--border-color);
+}
+
+.table > .header {
+    position: -webkit-sticky;
+    top: 0;
+    height: var(--navigation-bar-height);
+    line-height: calc(var(--navigation-bar-height) - 1px);
+    border-bottom: 1px solid var(--border-color);
+    background: white;
+    vertical-align: middle;
+}
+
+.table > .header > .sortable:active {
+    background-color: hsl(0, 0%, 70%);
+}
+
+.table > .header > :matches(.sort-ascending, .sort-descending) {
+    background-color: hsl(0, 0%, 90%);
+    -webkit-padding-end: 18px;
+}
+
+.table > .header > :matches(.sort-ascending, .sort-descending)::after {
+    position: absolute;
+    top: 1px;
+    bottom: 0;
+    width: 9px;
+    height: 8px;
+    margin-bottom: auto;
+    margin-top: auto;
+    content: "";
+    background-size: 9px 8px;
+    background-repeat: no-repeat;
+}
+
+body[dir=ltr] .table > .header > :matches(.sort-ascending, .sort-descending)::after {
+    right: 6px;
+}
+
+body[dir=rtl] .table > .header > :matches(.sort-ascending, .sort-descending)::after {
+    left: 6px;
+}
+
+.table > .header > .sort-ascending::after {
+    background-image: url(../Images/SortIndicatorArrows.svg#up-arrow-normal);
+}
+
+.table > .header > .sort-descending::after {
+    background-image: url(../Images/SortIndicatorArrows.svg#down-arrow-normal);
+}
+
+.table > .data-container {
+    position: absolute;
+    top: var(--navigation-bar-height);
+    bottom: 0;
+    left: 0;
+    right: 0;
+    overflow-x: hidden;
+    overflow-y: scroll;
+}
+
+.table > .data-container.not-scrollable {
+    overflow-y: hidden;
+}
+
+.table > .data-container > .data-list {
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+    min-height: 100%;
+
+    background-image: linear-gradient(to bottom, var(--even-zebra-stripe-row-background-color), var(--even-zebra-stripe-row-background-color) 50%, var(--odd-zebra-stripe-row-background-color) 50%, var(--odd-zebra-stripe-row-background-color));
+    background-size: 100% 40px;
+}
+
+.table > .data-container > .data-list.odd-first-zebra-stripe {
+    background-image: linear-gradient(to bottom, var(--odd-zebra-stripe-row-background-color), var(--odd-zebra-stripe-row-background-color) 50%, var(--even-zebra-stripe-row-background-color) 50%, var(--even-zebra-stripe-row-background-color));
+}
+
+.table > .data-container > .data-list > li {
+    height: 20px;
+    line-height: 20px;
+    vertical-align: middle;
+}
+
+.table > .data-container > .data-list > li.selected {
+    background-color: var(--selected-background-color-unfocused) !important;
+    color: inherit !important;
+}
+
+.table:focus > .data-container > .data-list li.selected {
+    background-color: var(--selected-background-color) !important;
+    color: var(--selected-foreground-color) !important;
+}
+
+.table .cell {
+    position: relative;
+    display: inline-block;
+    padding: 0 6px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+body[dir=ltr] .table .cell:not(:last-child) {
+    border-right: var(--table-column-border-end);
+}
+
+body[dir=rtl] .table .cell:not(:last-child) {
+    border-left: var(--table-column-border-end);
+}
+
+body[dir=ltr] .table .cell:first-child {
+    border-left: var(--table-column-border-start);
+}
+
+body[dir=rtl] .table .cell:first-child {
+    border-right: var(--table-column-border-start);
+}
+
+.table :not(.header) .cell:first-of-type {
+    background: rgba(0, 0, 0, 0.07);
+}
+
+.table .cell.align-right {
+    text-align: right;
+}
+
+.table .cell.align-left {
+    text-align: left;
+}
+
+.table .cell.align-center {
+    text-align: center;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/Table.js b/Source/WebInspectorUI/UserInterface/Views/Table.js
new file mode 100644 (file)
index 0000000..d409f2f
--- /dev/null
@@ -0,0 +1,1106 @@
+/*
+ * Copyright (C) 2008-2017 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.Table = class Table extends WI.View
+{
+    constructor(identifier, dataSource, delegate, rowHeight)
+    {
+        super();
+
+        console.assert(typeof identifier === "string");
+        console.assert(dataSource);
+        console.assert(delegate);
+        console.assert(rowHeight > 0);
+
+        this._identifier = identifier;
+        this._dataSource = dataSource;
+        this._delegate = delegate;
+        this._rowHeight = rowHeight;
+
+        // FIXME: Should be able to horizontally scroll non-locked table contents.
+        // To do this smoothly (without tearing) will require synchronous scroll events, or
+        // synchronized scrolling between multiple elements, or making `position: sticky`
+        // respect different vertical / horizontal scroll containers.
+
+        this.element.classList.add("table");
+        this.element.tabIndex = 0;
+        this.element.addEventListener("keydown", this._handleKeyDown.bind(this));
+
+        this._headerElement = this.element.appendChild(document.createElement("div"));
+        this._headerElement.className = "header";
+
+        let scrollHandler = this._handleScroll.bind(this);
+        this._scrollContainerElement = this.element.appendChild(document.createElement("div"));
+        this._scrollContainerElement.className = "data-container";
+        this._scrollContainerElement.addEventListener("scroll", scrollHandler);
+        this._scrollContainerElement.addEventListener("mousewheel", scrollHandler);
+        if (this._delegate.tableCellClicked)
+            this._scrollContainerElement.addEventListener("click", this._handleClick.bind(this));
+        if (this._delegate.tableCellContextMenuClicked)
+            this._scrollContainerElement.addEventListener("contextmenu", this._handleContextMenu.bind(this));
+
+        this._topSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
+        this._topSpacerElement.className = "spacer";
+
+        this._listElement = this._scrollContainerElement.appendChild(document.createElement("ul"));
+        this._listElement.className = "data-list";
+
+        this._bottomSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
+        this._bottomSpacerElement.className = "spacer";
+
+        this._fillerRow = this._listElement.appendChild(document.createElement("li"));
+        this._fillerRow.className = "filler";
+
+        this._cachedRows = new Map;
+
+        this._columnSpecs = new Map;
+        this._columnOrder = [];
+        this._visibleColumns = [];
+        this._hiddenColumns = [];
+
+        this._columnWidths = null; // Calculated in _resizeColumnsAndFiller.
+        this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
+
+        this._selectedRowIndex = NaN;
+
+        this._resizers = [];
+        this._currentResizer = null;
+        this._resizeLeftColumns = null;
+        this._resizeRightColumns = null;
+        this._resizeOriginalColumnWidths = null;
+        this._lastColumnIndexToAcceptRemainderPixel = 0;
+
+        this._sortOrder = WI.Table.SortOrder.Indeterminate;
+        this._sortColumnIdentifier = null;
+        this._sortRequestIdentifier = undefined;
+
+        this._sortOrderSetting = new WI.Setting(this._identifier + "-sort-order", this._sortOrder);
+        this._sortColumnIdentifierSetting = new WI.Setting(this._identifier + "-sort", this._sortColumnIdentifier);
+        this._columnVisibilitySetting = new WI.Setting(this._identifier + "-column-visibility", {});
+
+        this._cachedScrollTop = NaN;
+        this._cachedScrollableOffsetHeight = NaN;
+        this._previousRevealedRowCount = NaN;
+        this._topSpacerHeight = NaN;
+        this._bottomSpacerHeight = NaN;
+        this._visibleRowIndexStart = NaN;
+        this._visibleRowIndexEnd = NaN;
+
+        console.assert(this._delegate.tablePopulateCell, "Table delegate must implement tablePopulateCell.");
+    }
+
+    // Public
+
+    get identifier() { return this._identifier; }
+    get dataSource() { return this._dataSource; }
+    get delegate() { return this._delegate; }
+    get rowHeight() { return this._rowHeight; }
+    get selectedRow() { return this._selectedRowIndex; }
+
+    get sortOrder()
+    {
+        return this._sortOrder;
+    }
+
+    set sortOrder(sortOrder)
+    {
+        if (sortOrder === this._sortOrder)
+            return;
+
+        console.assert(sortOrder === WI.Table.SortOrder.Indeterminate || sortOrder === WI.Table.SortOrder.Ascending || sortOrder === WI.Table.SortOrder.Descending);
+
+        this._sortOrder = sortOrder;
+        this._sortOrderSetting.value = sortOrder;
+
+        if (this._sortColumnIdentifier) {
+            let column = this._columnSpecs.get(this._sortColumnIdentifier);
+            let columnIndex = this._visibleColumns.indexOf(column);
+            if (columnIndex !== -1) {
+                let headerCell = this._headerElement.children[columnIndex];
+                headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
+                headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
+            }
+
+            if (this._dataSource.tableSortChanged)
+                this._dataSource.tableSortChanged(this);
+        }
+    }
+
+    get sortColumnIdentifier()
+    {
+        return this._sortColumnIdentifier;
+    }
+
+    set sortColumnIdentifier(columnIdentifier)
+    {
+        if (columnIdentifier === this._sortColumnIdentifier)
+            return;
+
+        let column = this._columnSpecs.get(columnIdentifier);
+
+        console.assert(column, "Column not found.", columnIdentifier);
+        if (!column)
+            return;
+
+        console.assert(column.sortable, "Column is not sortable.", columnIdentifier);
+        if (!column.sortable)
+            return;
+
+        let oldSortColumnIdentifier = this._sortColumnIdentifier;
+        this._sortColumnIdentifier = columnIdentifier;
+        this._sortColumnIdentifierSetting.value = columnIdentifier;
+
+        if (oldSortColumnIdentifier) {
+            let oldColumn = this._columnSpecs.get(oldSortColumnIdentifier);
+            let oldColumnIndex = this._visibleColumns.indexOf(oldColumn);
+            if (oldColumnIndex !== -1) {
+                let headerCell = this._headerElement.children[oldColumnIndex];
+                headerCell.classList.remove("sort-ascending", "sort-descending");
+            }
+        }
+
+        if (this._sortColumnIdentifier) {
+            let newColumnIndex = this._visibleColumns.indexOf(column);
+            if (newColumnIndex !== -1) {
+                let headerCell = this._headerElement.children[newColumnIndex];
+                headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
+                headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
+            } else
+                this._sortColumnIdentifier = null;
+        }
+
+        if (this._dataSource.tableSortChanged)
+            this._dataSource.tableSortChanged(this);
+    }
+
+    resize()
+    {
+        this._resizeColumnsAndFiller();
+    }
+
+    reloadData()
+    {
+        this._cachedRows.clear();
+
+        this._previousRevealedRowCount = NaN;
+
+        this.needsLayout();
+    }
+
+    reloadDataAddedToEndOnly()
+    {
+        this._previousRevealedRowCount = NaN;
+        this.needsLayout();
+    }
+
+    reloadRow(rowIndex)
+    {
+        // Visible row, repopulate the cell.
+        if (this._isRowVisible(rowIndex)) {
+            let row = this._cachedRows.get(rowIndex);
+            if (!row)
+                return;
+            this._populateRow(row);
+            return;
+        }
+
+        // Non-visible row, will populate when it becomes visible.
+        this._cachedRows.delete(rowIndex);
+    }
+
+    reloadCell(rowIndex, columnIdentifier)
+    {
+        let column = this._columnSpecs.get(columnIdentifier);
+        let columnIndex = this._visibleColumns.indexOf(column);
+        if (columnIndex === -1)
+            return;
+
+        // Visible row, repopulate the cell.
+        if (this._isRowVisible(rowIndex)) {
+            let row = this._cachedRows.get(rowIndex);
+            if (!row)
+                return;
+            let cell = row.children[columnIndex];
+            if (!cell)
+                return;
+            this._delegate.tablePopulateCell(this, cell, column, rowIndex);
+            return;
+        }
+
+        // Non-visible row, will populate when it becomes visible.
+        this._cachedRows.delete(rowIndex);
+    }
+
+    selectRow(rowIndex)
+    {
+        if (this._selectedRowIndex === rowIndex)
+            return;
+
+        let oldSelectedRow = this._cachedRows.get(this._selectedRowIndex);
+        if (oldSelectedRow)
+            oldSelectedRow.classList.remove("selected");
+
+        this._selectedRowIndex = rowIndex;
+
+        let newSelectedRow = this._cachedRows.get(this._selectedRowIndex)
+        if (newSelectedRow)
+            newSelectedRow.classList.add("selected");
+
+        if (this._delegate.tableSelectedRowChanged)
+            this._delegate.tableSelectedRowChanged(this, this._selectedRowIndex);
+    }
+
+    clearSelectedRow()
+    {
+        if (isNaN(this._selectedRowIndex))
+            return;
+
+        let oldSelectedRow = this._cachedRows.get(this._selectedRowIndex);
+        if (oldSelectedRow)
+            oldSelectedRow.classList.remove("selected");
+
+        this._selectedRowIndex = NaN;
+    }
+
+    columnWithIdentifier(identifier)
+    {
+        return this._columnSpecs.get(identifier);
+    }
+
+    addColumn(column)
+    {
+        this._columnSpecs.set(column.identifier, column);
+        this._columnOrder.push(column.identifier);
+
+        if (column.hidden) {
+            this._hiddenColumns.push(column);
+            column.width = NaN;
+        } else {
+            this._visibleColumns.push(column);
+            this._headerElement.appendChild(this._createHeaderCell(column));
+            this._fillerRow.appendChild(this._createFillerCell(column));
+        }
+
+        // Restore saved user-specified column visibility.
+        let savedColumnVisibility = this._columnVisibilitySetting.value;
+        if (column.identifier in savedColumnVisibility) {
+            let visible = savedColumnVisibility[column.identifier];
+            if (visible)
+                this.showColumn(column);
+            else
+                this.hideColumn(column);
+        }
+
+        this.reloadData();
+    }
+
+    showColumn(column)
+    {
+        console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
+        console.assert(!column.locked, "Locked columns should always be shown.");
+        if (column.locked)
+            return;
+
+        if (!column.hidden)
+            return;
+
+        column.setHidden(false);
+
+        let columnIndex = this._hiddenColumns.indexOf(column);
+        this._hiddenColumns.splice(columnIndex, 1);
+
+        let newColumnIndex = this._indexToInsertColumn(column);
+        this._visibleColumns.insertAtIndex(column, newColumnIndex);
+
+        // Save user preference for this column to be visible.
+        let savedColumnVisibility = this._columnVisibilitySetting.value;
+        if (savedColumnVisibility[column.identifier] !== true) {
+            let copy = Object.shallowCopy(savedColumnVisibility);
+            if (column.defaultHidden)
+                copy[column.identifier] = true;
+            else
+                delete copy[column.identifier];
+            this._columnVisibilitySetting.value = copy;
+        }
+
+        this._headerElement.insertBefore(this._createHeaderCell(column), this._headerElement.children[newColumnIndex]);
+        this._fillerRow.insertBefore(this._createFillerCell(column), this._fillerRow.children[newColumnIndex]);
+
+        // We haven't yet done any layout, nothing to do.
+        if (!this._columnWidths)
+            return;
+
+        // To avoid recreating all the cells in the row we create empty cells,
+        // size them, and then populate them. We always populate a cell after
+        // it has been sized.
+        let cellsToPopulate = [];
+        for (let row of this._listElement.children) {
+            if (row !== this._fillerRow) {
+                let unpopulatedCell = this._createCell(column, newColumnIndex);
+                cellsToPopulate.push(unpopulatedCell);
+                row.insertBefore(unpopulatedCell, row.children[newColumnIndex]);
+            }
+        }
+
+        // Re-layout all columns to make space.
+        this._columnWidths = null;
+        this.resize();
+
+        // Now populate only the new cells for this column.
+        for (let cell of cellsToPopulate)
+            this._delegate.tablePopulateCell(this, cell, column, cell.parentElement.__index);
+    }
+
+    hideColumn(column)
+    {
+        console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
+        console.assert(!column.locked, "Locked columns should always be shown.");
+        if (column.locked)
+            return;
+
+        if (column.hidden)
+            return;
+
+        column.setHidden(true);
+
+        this._hiddenColumns.push(column);
+
+        let columnIndex = this._visibleColumns.indexOf(column);
+        this._visibleColumns.splice(columnIndex, 1);
+
+        // Save user preference for this column to be hidden.
+        let savedColumnVisibility = this._columnVisibilitySetting.value;
+        if (savedColumnVisibility[column.identifier] !== false) {
+            let copy = Object.shallowCopy(savedColumnVisibility);
+            if (column.defaultHidden)
+                delete copy[column.identifier];
+            else
+                copy[column.identifier] = false;
+            this._columnVisibilitySetting.value = copy;
+        }
+
+        this._headerElement.removeChild(this._headerElement.children[columnIndex]);
+        this._fillerRow.removeChild(this._fillerRow.children[columnIndex]);
+
+        // We haven't yet done any layout, nothing to do.
+        if (!this._columnWidths)
+            return;
+
+        this._columnWidths.splice(columnIndex, 1);
+
+        for (let row of this._listElement.children) {
+            if (row !== this._fillerRow)
+                row.removeChild(row.children[columnIndex]);
+        }
+
+        this.needsLayout();
+    }
+
+    restoreScrollPosition()
+    {
+        if (this._cachedScrollTop && !this._scrollContainerElement.scrollTop)
+            this._scrollContainerElement.scrollTop = this._cachedScrollTop;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        this.sortOrder = this._sortOrderSetting.value;
+
+        let restoreSortColumnIdentifier = this._sortColumnIdentifierSetting.value;
+        if (!this._columnSpecs.has(restoreSortColumnIdentifier))
+            this._sortColumnIdentifierSetting.value = null;
+        else
+            this.sortColumnIdentifier = restoreSortColumnIdentifier;
+    }
+
+    layout()
+    {
+        this._updateVisibleRows();
+
+        this.resize();
+    }
+
+    // Resizer delegate
+
+    resizerDragStarted(resizer)
+    {
+        console.assert(!this._currentResizer, resizer, this._currentResizer);
+
+        let resizerIndex = this._resizers.indexOf(resizer);
+
+        this._currentResizer = resizer;
+        this._resizeLeftColumns = this._visibleColumns.slice(0, resizerIndex + 1).reverse(); // Reversed to simplify iteration.
+        this._resizeRightColumns = this._visibleColumns.slice(resizerIndex + 1);
+        this._resizeOriginalColumnWidths = [].concat(this._columnWidths);
+    }
+
+    resizerDragging(resizer, positionDelta)
+    {
+        console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
+        if (resizer !== this._currentResizer)
+            return;
+
+        if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
+            positionDelta = -positionDelta;
+
+        // Completely recalculate columns from the original sizes based on the new mouse position.
+        this._columnWidths = [].concat(this._resizeOriginalColumnWidths);
+
+        if (!positionDelta) {
+            this._applyColumnWidths();
+            return;
+        }
+
+        let delta = Math.abs(positionDelta);
+        let leftDirection = positionDelta > 0;
+        let rightDirection = !leftDirection;
+
+        let columnWidths = this._columnWidths;
+        let visibleColumns = this._visibleColumns;
+
+        function growableSize(column) {
+            let width = columnWidths[visibleColumns.indexOf(column)];
+            if (column.maxWidth)
+                return column.maxWidth - width;
+            return Infinity;
+        }
+
+        function shrinkableSize(column) {
+            let width = columnWidths[visibleColumns.indexOf(column)];
+            if (column.minWidth)
+                return width - column.minWidth;
+            return width;
+        }
+
+        function canGrow(column) {
+            return growableSize(column) > 0;
+        }
+
+        function canShrink(column) {
+            return shrinkableSize(column) > 0;
+        }
+
+        function columnToResize(columns, isShrinking) {
+            // First find a flexible column we can resize.
+            for (let column of columns) {
+                if (!column.flexible)
+                    continue;
+                if (isShrinking ? canShrink(column) : canGrow(column))
+                    return column;
+            }
+
+            // Failing that see if we can resize the immediately neighbor.
+            let immediateColumn = columns[0];
+            if ((isShrinking && canShrink(immediateColumn)) || (!isShrinking && canGrow(immediateColumn)))
+                return immediateColumn;
+
+            // Bail. There isn't anything obvious in the table that can resize.
+            return null;
+        }
+
+        while (delta > 0) {
+            let leftColumn = columnToResize(this._resizeLeftColumns, leftDirection);
+            let rightColumn = columnToResize(this._resizeRightColumns, rightDirection);
+            if (!leftColumn || !rightColumn) {
+                // No more left or right column to grow or shrink.
+                break;
+            }
+
+            let incrementalDelta = Math.min(delta,
+                leftDirection ? shrinkableSize(leftColumn) : shrinkableSize(rightColumn),
+                leftDirection ? growableSize(rightColumn) : growableSize(leftColumn));
+
+            let leftIndex = this._visibleColumns.indexOf(leftColumn);
+            let rightIndex = this._visibleColumns.indexOf(rightColumn);
+
+            if (leftDirection) {
+                this._columnWidths[leftIndex] -= incrementalDelta;
+                this._columnWidths[rightIndex] += incrementalDelta;
+            } else {
+                this._columnWidths[leftIndex] += incrementalDelta;
+                this._columnWidths[rightIndex] -= incrementalDelta;
+            }
+
+            delta -= incrementalDelta;
+        }
+
+        this._applyColumnWidths();
+    }
+
+    resizerDragEnded(resizer)
+    {
+        console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
+        if (resizer !== this._currentResizer)
+            return;
+
+        this._currentResizer = null;
+        this._resizeLeftColumns = null;
+        this._resizeRightColumns = null;
+        this._resizeOriginalColumnWidths = null;
+
+        this._positionResizerElements();
+    }
+
+    // Private
+
+    _createHeaderCell(column)
+    {
+        let cell = document.createElement("span");
+        cell.classList.add("cell", column.identifier);
+        cell.textContent = column.name;
+
+        if (column.align)
+            cell.classList.add("align-" + column.align);
+        if (column.sortable) {
+            cell.classList.add("sortable");
+            cell.addEventListener("click", this._handleHeaderCellClicked.bind(this, column));
+        }
+
+        cell.addEventListener("contextmenu", this._handleHeaderContextMenu.bind(this, column));
+
+        return cell;
+    }
+
+    _createFillerCell(column)
+    {
+        let cell = document.createElement("span");
+        cell.classList.add("cell", column.identifier);
+        return cell;
+    }
+
+    _createCell(column, columnIndex)
+    {
+        let cell = document.createElement("span");
+        cell.classList.add("cell", column.identifier);
+        if (column.align)
+            cell.classList.add("align-" + column.align);
+        if (this._columnWidths)
+            cell.style.width = this._columnWidths[columnIndex] + "px";
+        return cell;
+    }
+
+    _getOrCreateRow(rowIndex)
+    {
+        let cachedRow = this._cachedRows.get(rowIndex);
+        if (cachedRow)
+            return cachedRow;
+
+        let row = document.createElement("li");
+        row.__index = rowIndex;
+        if (rowIndex === this._selectedRowIndex)
+            row.classList.add("selected");
+
+        this._cachedRows.set(rowIndex, row);
+        return row;
+    }
+
+    _populatedCellForColumnAndRow(column, columnIndex, rowIndex)
+    {
+        console.assert(rowIndex !== undefined, "Tried to populate a row that did not know its index. Is this the filler row?");
+
+        let cell = this._createCell(column, columnIndex);
+        this._delegate.tablePopulateCell(this, cell, column, rowIndex);
+        return cell;
+    }
+
+    _populateRow(row)
+    {
+        row.removeChildren();
+
+        let rowIndex = row.__index;
+        for (let i = 0; i < this._visibleColumns.length; ++i) {
+            let column = this._visibleColumns[i];
+            let cell = this._populatedCellForColumnAndRow(column, i, rowIndex);
+            row.appendChild(cell);
+        }
+    }
+
+    _resizeColumnsAndFiller()
+    {
+        this._fillerRow.remove();
+
+        let availableWidth = this._listElement.offsetWidth;
+        let availableHeight = this._listElement.offsetHeight;
+
+        // Not visible yet.
+        if (!availableWidth)
+            return;
+
+        let numberOfRows = this._dataSource.tableNumberOfRows(this);
+        let contentHeight = numberOfRows * this._rowHeight;
+        this._fillerHeight = Math.max(availableHeight - contentHeight, 0);
+
+        let lockedWidth = 0;
+        let lockedColumnCount = 0;
+        let totalMinimumWidth = 0;
+
+        for (let column of this._visibleColumns) {
+            if (column.locked) {
+                lockedWidth += column.width;
+                lockedColumnCount++;
+                totalMinimumWidth += column.width;
+            } else if (column.minWidth)
+                totalMinimumWidth += column.minWidth;
+        }
+
+        let flexibleWidth = availableWidth - lockedWidth;
+        let flexibleColumnCount = this._visibleColumns.length - lockedColumnCount;
+
+        // NOTE: We will often distribute pixels evenly across flexible columns in the table.
+        // If `availableWidth < totalMinimumWidth` than the table is too small for the minimum
+        // sizes of all the columns and we will start crunching the table (removing pixels from
+        // all flexible columns). This would be the appropriate time to introduce horizontal
+        // scrolling. For now we just remove pixels evenly.
+        //
+        // When distributing pixels, always start from the last column to accept remainder
+        // pixels so we don't always add from one side / to one column.
+        function distributeRemainingPixels(remainder, shrinking) {
+            // No pixels to distribute.
+            if (!remainder)
+                return;
+
+            let indexToStartAddingRemainderPixels = (this._lastColumnIndexToAcceptRemainderPixel + 1) % this._visibleColumns.length;
+
+            // Handle tables that are too small or too large. If the size constraints
+            // cause the columns to be too small or large. A second pass will do the
+            // expanding or crunching ignoring constraints.
+            let ignoreConstraints = false;
+
+            while (remainder > 0) {
+                let initialRemainder = remainder;
+
+                for (let i = indexToStartAddingRemainderPixels; i < this._columnWidths.length; ++i) {
+                    let column = this._visibleColumns[i];
+                    if (column.locked)
+                        continue;
+
+                    if (shrinking) {
+                        if (ignoreConstraints || (column.minWidth && this._columnWidths[i] > column.minWidth)) {
+                            this._columnWidths[i]--;
+                            remainder--;
+                        }
+                    } else {
+                        if (ignoreConstraints || (column.maxWidth && this._columnWidths[i] < column.maxWidth)) {
+                            this._columnWidths[i]++;
+                            remainder--;
+                        } else if (!column.maxWidth) {
+                            this._columnWidths[i]++;
+                            remainder--;
+                        }
+                    }
+
+                    if (!remainder) {
+                        this._lastColumnIndexToAcceptRemainderPixel = i;
+                        break;
+                    }
+                }
+
+                if (remainder === initialRemainder && !indexToStartAddingRemainderPixels) {
+                    // We have remaining pixels. Start crunching if we need to.
+                    if (ignoreConstraints)
+                        break;
+                    ignoreConstraints = true;
+                }
+
+                indexToStartAddingRemainderPixels = 0;
+            }
+
+            console.assert(!remainder, "Should not have undistributed pixels.");
+        }
+
+        // Two kinds of layouts. Autosize or Resize.
+        if (!this._columnWidths) {
+            // Autosize: Flex all the flexes evenly and trickle out any remaining pixels.
+            this._columnWidths = [];
+            this._lastColumnIndexToAcceptRemainderPixel = 0;
+
+            let bestFitWidth = 0;
+            let bestFitColumnCount = 0;
+
+            function bestFit(callback) {
+                while (true) {
+                    let remainingFlexibleColumnCount = flexibleColumnCount - bestFitColumnCount;
+                    if (!remainingFlexibleColumnCount)
+                        return;
+
+                    // Fair size to give each flexible column.
+                    let remainingFlexibleWidth = flexibleWidth - bestFitWidth;
+                    let flexWidth = Math.floor(remainingFlexibleWidth / remainingFlexibleColumnCount);
+
+                    let didPerformBestFit = false;
+                    for (let i = 0; i < this._visibleColumns.length; ++i) {
+                        // Already best fit this column.
+                        if (this._columnWidths[i])
+                            continue;
+
+                        let column = this._visibleColumns[i];
+                        console.assert(column.flexible, "Non-flexible columns should have been sized earlier", column);
+
+                        // Attempt best fit.
+                        let bestWidth = callback(column, flexWidth);
+                        if (bestWidth === -1)
+                            continue;
+
+                        this._columnWidths[i] = bestWidth;
+                        bestFitWidth += bestWidth;
+                        bestFitColumnCount++;
+                        didPerformBestFit = true;
+                    }
+                    if (!didPerformBestFit)
+                        return;
+
+                    // Repeat with a new flex size now that we have fewer flexible columns.
+                }
+            }
+
+            // Fit the locked columns.
+            for (let i = 0; i < this._visibleColumns.length; ++i) {
+                let column = this._visibleColumns[i];
+                if (column.locked)
+                    this._columnWidths[i] = column.width;
+            }
+
+            // Best fit max size flexible columns. May make more pixels available for other columns.
+            bestFit.call(this, (column, width) => {
+                if (!column.maxWidth || width <= column.maxWidth)
+                    return -1;
+                return column.maxWidth;
+            });
+
+            // Best fit min size flexible columns. May make less pixels available for other columns.
+            bestFit.call(this, (column, width) => {
+                if (!column.minWidth || width >= column.minWidth)
+                    return -1;
+                return column.minWidth;
+            });
+
+            // Best fit the remaining flexible columns with the fair remaining size.
+            bestFit.call(this, (column, width) => width);
+
+            // Distribute any remaining pixels evenly.
+            let remainder = availableWidth - (lockedWidth + bestFitWidth);
+            let shrinking = remainder < 0;
+            distributeRemainingPixels.call(this, Math.abs(remainder), shrinking);
+        } else {
+            // Resize: Distribute pixels evenly across flex columns.
+            console.assert(this._columnWidths.length === this._visibleColumns.length, "Number of columns should not change in a resize.");
+
+            let originalTotalColumnWidth = 0;
+            for (let width of this._columnWidths)
+                originalTotalColumnWidth += width;
+
+            let remainder = Math.abs(availableWidth - originalTotalColumnWidth);
+            let shrinking = availableWidth < originalTotalColumnWidth;
+            distributeRemainingPixels.call(this, remainder, shrinking);
+        }
+
+        // Apply widths.
+
+        if (this._fillerHeight > 0) {
+            const heightPastEdge = 100; // Extend past edge some reasonable amount.
+            this._fillerHeight += this._rowHeight + heightPastEdge;
+            this._scrollContainerElement.classList.add("not-scrollable");
+            this._listElement.appendChild(this._fillerRow);
+        } else
+            this._scrollContainerElement.classList.remove("not-scrollable");
+
+        this._applyColumnWidths();
+        this._positionResizerElements();
+    }
+
+    _updateVisibleRows()
+    {
+        let rowHeight = this._rowHeight;
+        let updateOffsetThreshold = rowHeight * 10;
+        let overflowPadding = updateOffsetThreshold * 3;
+
+        if (isNaN(this._cachedScrollTop))
+            this._cachedScrollTop = this._scrollContainerElement.scrollTop;
+
+        if (isNaN(this._cachedScrollableOffsetHeight) || !this._cachedScrollableOffsetHeight)
+            this._cachedScrollableOffsetHeight = this._scrollContainerElement.offsetHeight;
+
+        let scrollTop = this._cachedScrollTop;
+        let scrollableOffsetHeight = this._cachedScrollableOffsetHeight;
+
+        let visibleRowCount = Math.ceil((scrollableOffsetHeight + (overflowPadding * 2)) / rowHeight);
+        let currentTopMargin = this._topSpacerHeight;
+        let currentBottomMargin = this._bottomSpacerHeight;
+        let currentTableBottom = currentTopMargin + (visibleRowCount * rowHeight);
+
+        let belowTopThreshold = !currentTopMargin || scrollTop > currentTopMargin + updateOffsetThreshold;
+        let aboveBottomThreshold = !currentBottomMargin || scrollTop + scrollableOffsetHeight < currentTableBottom - updateOffsetThreshold;
+
+        if (belowTopThreshold && aboveBottomThreshold && !isNaN(this._previousRevealedRowCount))
+            return;
+
+        let numberOfRows = this._dataSource.tableNumberOfRows(this);
+        this._previousRevealedRowCount = numberOfRows;
+
+        let topHiddenRowCount = Math.max(0, Math.floor((scrollTop - overflowPadding) / rowHeight));
+        let bottomHiddenRowCount = Math.max(0, this._previousRevealedRowCount - topHiddenRowCount - visibleRowCount);
+
+        let marginTop = topHiddenRowCount * rowHeight;
+        let marginBottom = bottomHiddenRowCount * rowHeight;
+
+        if (this._topSpacerHeight !== marginTop) {
+            this._topSpacerHeight = marginTop;
+            this._topSpacerElement.style.height = marginTop + "px";
+        }
+
+        if (this._bottomDataTableMarginElement !== marginBottom) {
+            this._bottomSpacerHeight = marginBottom;
+            this._bottomSpacerElement.style.height = marginBottom + "px";
+        }
+
+        this._visibleRowIndexStart = topHiddenRowCount;
+        this._visibleRowIndexEnd = this._visibleRowIndexStart + visibleRowCount;
+
+        this._listElement.removeChildren();
+        this._listElement.classList.toggle("odd-first-zebra-stripe", !!(topHiddenRowCount % 2));
+
+        for (let i = this._visibleRowIndexStart; i < this._visibleRowIndexEnd && i < numberOfRows; ++i) {
+            let row = this._getOrCreateRow(i);
+            this._listElement.appendChild(row);
+        }
+
+        this._listElement.appendChild(this._fillerRow);
+    }
+
+    _applyColumnWidths()
+    {
+        for (let i = 0; i < this._visibleColumns.length; ++i)
+            this._visibleColumns[i].width = this._columnWidths[i];
+
+        for (let i = 0; i < this._headerElement.children.length; ++i)
+            this._headerElement.children[i].style.width = this._columnWidths[i] + "px";
+
+        for (let row of this._listElement.children) {
+            for (let i = 0; i < row.children.length; ++i)
+                row.children[i].style.width = this._columnWidths[i] + "px";
+        }
+
+        for (let cell of this._fillerRow.children)
+            cell.style.height = this._fillerHeight + "px";
+
+        // Create missing cells after we've sized.
+        for (let row of this._listElement.children) {
+            if (row !== this._fillerRow) {
+                if (row.children.length !== this._visibleColumns.length)
+                    this._populateRow(row);
+            }
+        }
+    }
+
+    _positionResizerElements()
+    {
+        console.assert(this._visibleColumns.length === this._columnWidths.length);
+
+        // Create the appropriate number of resizers.
+        let resizersNeededCount = this._visibleColumns.length - 1;
+        if (this._resizers.length !== resizersNeededCount) {
+            if (this._resizers.length < resizersNeededCount) {
+                do {
+                    let resizer = new WI.Resizer(WI.Resizer.RuleOrientation.Vertical, this);
+                    this._resizers.push(resizer);
+                    this.element.appendChild(resizer.element);
+                } while (this._resizers.length < resizersNeededCount);
+            } else {
+                do {
+                    let resizer = this._resizers.pop();
+                    this.element.removeChild(resizer.element);
+                } while (this._resizers.length > resizersNeededCount);
+            }
+        }
+
+        // Position them.
+        const columnResizerAdjustment = 3;
+        let positionAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
+        let totalWidth = 0;
+        for (let i = 0; i < resizersNeededCount; ++i) {
+            totalWidth += this._columnWidths[i];
+            this._resizers[i].element.style[positionAttribute] = (totalWidth - columnResizerAdjustment) + "px";
+        }
+    }
+
+    _isRowVisible(rowIndex)
+    {
+        if (!this._previousRevealedRowCount)
+            return false;
+
+        return rowIndex >= this._visibleRowIndexStart && rowIndex <= this._visibleRowIndexEnd;
+    }
+
+    _indexToInsertColumn(column)
+    {
+        let currentVisibleColumnIndex = 0;
+
+        for (let columnIdentifier of this._columnOrder) {
+            if (columnIdentifier === column.identifier)
+                return currentVisibleColumnIndex;
+            if (columnIdentifier === this._visibleColumns[currentVisibleColumnIndex].identifier) {
+                currentVisibleColumnIndex++;
+                if (currentVisibleColumnIndex >= this._visibleColumns.length)
+                    break;
+            }
+        }
+
+        return currentVisibleColumnIndex;
+    }
+
+    _handleScroll(event)
+    {
+        if (event.type === "mousewheel" && !event.wheelDeltaY)
+            return;
+
+        this._cachedScrollTop = NaN;
+        this.needsLayout();
+    }
+
+    _handleKeyDown(event)
+    {
+        if (!this._isRowVisible(this._selectedRowIndex))
+            return;
+
+        if (event.shiftKey || event.metaKey || event.ctrlKey)
+            return;
+
+        let rowToSelect = NaN;
+
+        if (event.keyIdentifier === "Up") {
+            if (this._selectedRowIndex > 0)
+                rowToSelect = this._selectedRowIndex - 1;
+        } else if (event.keyIdentifier === "Down") {
+            let numberOfRows = this._dataSource.tableNumberOfRows(this);
+            if (this._selectedRowIndex < (numberOfRows - 1))
+                rowToSelect = this._selectedRowIndex + 1;
+        }
+
+        if (!isNaN(rowToSelect)) {
+            this.selectRow(rowToSelect);
+
+            let row = this._cachedRows.get(this._selectedRowIndex);
+            console.assert(row, "Moving up or down by one should always find a cached row since it is within the overflow bounds.");
+            row.scrollIntoViewIfNeeded();
+
+            // Force our own scroll update because we may have scrolled.
+            this._cachedScrollTop = NaN;
+            this.needsLayout();
+
+            event.preventDefault();
+            event.stopPropagation();
+        }
+    }
+
+    _handleClick(event)
+    {
+        let cell = event.target.enclosingNodeOrSelfWithClass("cell");
+        if (!cell)
+            return;
+
+        let row = cell.parentElement;
+        if (row === this._fillerRow)
+            return;
+
+        let columnIndex = Array.from(row.children).indexOf(cell);
+        let column = this._visibleColumns[columnIndex];
+        let rowIndex = row.__index;
+
+        this._delegate.tableCellClicked(this, cell, column, rowIndex, event);
+    }
+
+    _handleContextMenu(event)
+    {
+        let cell = event.target.enclosingNodeOrSelfWithClass("cell");
+        if (!cell)
+            return;
+
+        let row = cell.parentElement;
+        if (row === this._fillerRow)
+            return;
+
+        let columnIndex = Array.from(row.children).indexOf(cell);
+        let column = this._visibleColumns[columnIndex];
+        let rowIndex = row.__index;
+
+        this._delegate.tableCellContextMenuClicked(this, cell, column, rowIndex, event);
+    }
+
+    _handleHeaderCellClicked(column, event)
+    {
+        let sortOrder = this._sortOrder;
+        if (sortOrder === WI.Table.SortOrder.Indeterminate)
+            sortOrder = WI.Table.SortOrder.Descending;
+        else if (this._sortColumnIdentifier === column.identifier)
+            sortOrder = sortOrder === WI.Table.SortOrder.Ascending ? WI.Table.SortOrder.Descending : WI.Table.SortOrder.Ascending;
+
+        this.sortColumnIdentifier = column.identifier;
+        this.sortOrder = sortOrder;
+    }
+
+    _handleHeaderContextMenu(column, event)
+    {
+        let contextMenu = WI.ContextMenu.createFromEvent(event);
+
+        if (column.sortable) {
+            if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Ascending) {
+                contextMenu.appendItem(WI.UIString("Sort Ascending"), () => {
+                    this.sortColumnIdentifier = column.identifier;
+                    this.sortOrder = WI.Table.SortOrder.Ascending;
+                });
+            }
+
+            if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Descending) {
+                contextMenu.appendItem(WI.UIString("Sort Descending"), () => {
+                    this.sortColumnIdentifier = column.identifier;
+                    this.sortOrder = WI.Table.SortOrder.Descending;
+                });
+            }
+        }
+
+        contextMenu.appendSeparator();
+
+        for (let [columnIdentifier, column] of this._columnSpecs) {
+            if (column.locked)
+                continue;
+
+            let checked = !column.hidden;
+            contextMenu.appendCheckboxItem(column.name, () => {
+                if (column.hidden)
+                    this.showColumn(column);
+                else
+                    this.hideColumn(column);
+            }, checked);
+        }
+    }
+};
+
+WI.Table.SortOrder = {
+    Indeterminate: "table-sort-order-indeterminate",
+    Ascending: "table-sort-order-ascending",
+    Descending: "table-sort-order-descending",
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/TableColumn.js b/Source/WebInspectorUI/UserInterface/Views/TableColumn.js
new file mode 100644 (file)
index 0000000..d021526
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.TableColumn = class TableColumn extends WI.Object
+{
+    constructor(identifier, name, {initialWidth, minWidth, maxWidth, hidden, sortable, align, resizeType} = {})
+    {
+        super();
+
+        console.assert(identifier);
+        console.assert(name);
+        console.assert(!initialWidth || initialWidth > 0);
+        console.assert(!minWidth || minWidth >= 0);
+        console.assert(!maxWidth || maxWidth >= 0);
+
+        this._identifier = identifier;
+        this._name = name;
+
+        this._width = initialWidth || NaN;
+        this._minWidth = minWidth || 50;
+        this._maxWidth = maxWidth || 0;
+        this._hidden = hidden || false;
+        this._defaultHidden = hidden || false;
+        this._sortable = typeof sortable === "boolean" ? sortable : true;
+        this._align = align || null;
+        this._resizeType = resizeType || TableColumn.ResizeType.Auto;
+
+        console.assert(!this._minWidth || !this._maxWidth || this._minWidth <= this._maxWidth, "Invalid min/max", this._minWidth, this._maxWidth);
+        console.assert(isNaN(this._width) || !this._minWidth || (this._width >= this._minWidth), "Initial width is less than min", this._width, this._minWidth);
+        console.assert(isNaN(this._width) || !this._maxWidth || (this._width <= this._maxWidth), "Initial width is greater than max", this._width, this._maxWidth);
+        console.assert(!this.locked || this.width, "A locked column should aways have an initial width");
+        console.assert(!this.locked || !this.hidden, "A locked column should never be hidden");
+    }
+
+    get identifier() { return this._identifier; }
+    get name() { return this._name; }
+    get minWidth() { return this._minWidth; }
+    get maxWidth() { return this._maxWidth; }
+    get defaultHidden() { return this._defaultHidden; }
+    get sortable() { return this._sortable; }
+    get align() { return this._align; }
+
+    get locked() { return this._resizeType === TableColumn.ResizeType.Locked; }
+    get flexible() { return this._resizeType === TableColumn.ResizeType.Auto; }
+
+    get width()
+    {
+        return this._width;
+    }
+
+    set width(width)
+    {
+        // NOTE: We can't assert this because we resize past the minimum and maximum sizes.
+        // If we support horizontal scrolling in the Table then we could assert these.
+        // console.assert(isNaN(width) || !this._minWidth || width >= this._minWidth, "New width was less than midWidth.", width, this._minWidth);
+        // console.assert(isNaN(width) || !this._maxWidth || width <= this._maxWidth, "New width was greater than maxWidth.", width, this._maxWidth);
+
+        if (this._width === width)
+            return;
+
+        this._width = width;
+
+        this.dispatchEventToListeners(WI.TableColumn.Event.WidthDidChange);
+    }
+
+    get hidden()
+    {
+        return this._hidden;
+    }
+
+    setHidden(x)
+    {
+        this._hidden = x;
+    }
+};
+
+WI.TableColumn.ResizeType = {
+    Auto: "auto",
+    Locked: "locked",
+};
+
+WI.TableColumn.Event = {
+    WidthDidChange: "table-column-width-did-change",
+};