-
Notifications
You must be signed in to change notification settings - Fork 6.6k
/
clusters.ts
296 lines (258 loc) · 9.61 KB
/
clusters.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './cluster.js';
import './shared_style.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.m.js';
import 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.m.js';
import {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {IronScrollThresholdElement} from 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserProxy} from './browser_proxy.js';
import {PageCallbackRouter, PageHandlerRemote, QueryParams, QueryResult, URLVisit} from './history_clusters.mojom-webui.js';
/**
* @fileoverview This file provides a custom element that requests and shows
* history clusters given a query. It handles loading more clusters using
* infinite scrolling as well as deletion of visits within the clusters.
*/
const RESULTS_PER_PAGE: number = 5;
declare global {
interface HTMLElementTagNameMap {
'history-clusters': HistoryClustersElement,
}
interface Window {
// https://github.com/microsoft/TypeScript/issues/40807
requestIdleCallback(callback: () => void): void;
}
}
interface HistoryClustersElement {
$: {
confirmationDialog: CrLazyRenderElement<CrDialogElement>,
confirmationToast: CrLazyRenderElement<CrToastElement>,
container: Element,
scrollThreshold: IronScrollThresholdElement,
};
}
class HistoryClustersElement extends PolymerElement {
static get is() {
return 'history-clusters';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/**
* The current query for which related Clusters are requested and shown.
*/
query: {
type: String,
observer: 'onQueryChanged_',
},
/**
* Contains 1) the Clusters returned by the browser in response to a
* request for the freshest Clusters related to a given query until a
* given time threshold and 2) the optional continuation query parameters
* returned alongside the Clusters to be used in the follow-up request to
* load older Clusters.
*/
result_: Object,
/**
* The title to show when the query is non-empty.
*/
title_: {
type: String,
computed: `computeTitle_(result_)`,
},
/**
* The list of visits to be removed. A non-empty array indicates a pending
* remove request to the browser.
*/
visitsToBeRemoved_: {
type: Object,
value: [],
},
};
}
//============================================================================
// Properties
//============================================================================
query: string = '';
private callbackRouter_: PageCallbackRouter;
private onClustersQueryResultListenerId_: number|null = null;
private onVisitsRemovedListenerId_: number|null = null;
private pageHandler_: PageHandlerRemote;
private result_: QueryResult = new QueryResult();
private title_: string = '';
private visitsToBeRemoved_: Array<URLVisit> = [];
//============================================================================
// Overridden methods
//============================================================================
constructor() {
super();
this.pageHandler_ = BrowserProxy.getInstance().handler;
this.callbackRouter_ = BrowserProxy.getInstance().callbackRouter;
}
connectedCallback() {
super.connectedCallback();
this.onClustersQueryResultListenerId_ =
this.callbackRouter_.onClustersQueryResult.addListener(
this.onClustersQueryResult_.bind(this));
this.onVisitsRemovedListenerId_ =
this.callbackRouter_.onVisitsRemoved.addListener(
this.onVisitsRemoved_.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
this.callbackRouter_.removeListener(
assert(this.onClustersQueryResultListenerId_!));
this.onClustersQueryResultListenerId_ = null;
this.callbackRouter_.removeListener(
assert(this.onVisitsRemovedListenerId_!));
this.onVisitsRemovedListenerId_ = null;
}
//============================================================================
// Event handlers
//============================================================================
private onCancelButtonClick_() {
this.visitsToBeRemoved_ = [];
this.$.confirmationDialog.get().close();
}
/**
* Called when an event is received from a cluster that should be removed or
* restructured due to all its visits or its top visit having been removed.
* Contains the id of the Cluster in question.
* @private
*/
private onClusterChangedOrRemoved_() {
// Request up to as many of the freshest clusters as currently shown until
// now.
this.onBrowserIdle_().then(() => {
this.queryClusters_({
query: this.query.trim(),
maxCount: this.result_.clusters.length,
endTime: undefined,
});
});
}
private onConfirmationDialogCancel_() {
this.visitsToBeRemoved_ = [];
}
private onRemoveButtonClick_() {
this.pageHandler_.removeVisits(this.visitsToBeRemoved_)
.then(({accepted}) => {
if (!accepted) {
this.visitsToBeRemoved_ = [];
}
});
this.$.confirmationDialog.get().close();
}
/**
* Called with `event` received from a visit requesting to be removed. `event`
* may contain the related visits of the said visit, if applicable.
*/
private onRemoveVisits_(event: CustomEvent<Array<URLVisit>>) {
// Return early if there is a pending remove request.
if (this.visitsToBeRemoved_.length) {
return;
}
this.visitsToBeRemoved_ = event.detail;
if (assert(this.visitsToBeRemoved_.length) > 1) {
this.$.confirmationDialog.get().showModal();
} else {
// Bypass the confirmation dialog if removing one visit only.
this.onRemoveButtonClick_();
}
}
/**
* Called when the value of the search field changes.
*/
private onSearchChanged_(event: CustomEvent<string>) {
// Update the query based on the value of the search field, if necessary.
if (event.detail !== this.query) {
this.query = event.detail;
}
}
/**
* Called when the scrollable area has been scrolled nearly to the bottom.
*/
private onScrolledToBottom_() {
this.$.scrollThreshold.clearTriggers();
if (this.result_ && this.result_.continuationEndTime) {
this.queryClusters_({
query: this.result_.query,
maxCount: RESULTS_PER_PAGE,
endTime: this.result_.continuationEndTime,
});
}
}
//============================================================================
// Helper methods
//============================================================================
private computeTitle_(): string {
return this.result_ ?
loadTimeData.getStringF('headerTitle', this.result_.query || '') :
'';
}
/**
* Returns a promise that resolves when the browser is idle.
*/
private onBrowserIdle_(): Promise<void> {
return new Promise(resolve => {
window.requestIdleCallback(() => {
resolve();
});
});
}
private onClustersQueryResult_(result: QueryResult) {
if (result.isContinuation) {
// Do not replace the existing result. `result` contains a partial set of
// Clusters that should be appended to the existing ones.
this.push('result_.clusters', ...result.clusters);
this.result_.continuationEndTime = result.continuationEndTime;
} else {
this.result_ = result;
}
}
private onQueryChanged_() {
this.onBrowserIdle_().then(() => {
// Request up to `RESULTS_PER_PAGE` of the freshest Clusters until now.
this.queryClusters_({
query: this.query.trim(),
maxCount: RESULTS_PER_PAGE,
endTime: undefined,
});
// Scroll to the top when the results change due to query change.
this.$.container.scrollTop = 0;
});
}
/**
* Called when the last accepted request to browser to remove visits succeeds.
*/
private onVisitsRemoved_() {
// Show the confirmation toast once done removing one visit only; since a
// confirmation dialog was not shown prior to the action.
if (assert(this.visitsToBeRemoved_.length) === 1) {
this.$.confirmationToast.get().show();
}
this.visitsToBeRemoved_ = [];
}
private queryClusters_(queryParams: QueryParams) {
// Invalidate the existing `continuationEndTime`, if any, in order to
// prevent sending additional requests while a request is in-flight. A new
// `continuationEndTime` will be supplied with the new set of results.
if (this.result_) {
this.result_.continuationEndTime = undefined;
}
this.pageHandler_.queryClusters(queryParams);
}
}
customElements.define(HistoryClustersElement.is, HistoryClustersElement);