| // Copyright 2019 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 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.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_icon_button/cr_icon_button.js'; |
| import 'chrome://resources/cr_elements/cr_icons_css.m.js'; |
| import 'chrome://resources/cr_elements/cr_input/cr_input.m.js'; |
| import 'chrome://resources/cr_elements/cr_toast/cr_toast.js'; |
| import 'chrome://resources/cr_elements/hidden_style_css.m.js'; |
| |
| import {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js'; |
| import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js'; |
| import {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js'; |
| import {assert} from 'chrome://resources/js/assert.m.js'; |
| import {skColorToRgba} from 'chrome://resources/js/color_utils.js'; |
| import {isMac} from 'chrome://resources/js/cr.m.js'; |
| import {FocusOutlineManager} from 'chrome://resources/js/cr/ui/focus_outline_manager.m.js'; |
| import {EventTracker} from 'chrome://resources/js/event_tracker.m.js'; |
| import {I18nMixin} from 'chrome://resources/js/i18n_mixin.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; |
| import {hasKeyModifiers} from 'chrome://resources/js/util.m.js'; |
| import {TextDirection} from 'chrome://resources/mojo/mojo/public/mojom/base/text_direction.mojom-webui.js'; |
| import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js'; |
| import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js'; |
| import {DomRepeat, DomRepeatEvent, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import {MostVisitedBrowserProxy} from './browser_proxy.js'; |
| import {getTemplate} from './most_visited.html.js'; |
| import {MostVisitedInfo, MostVisitedPageCallbackRouter, MostVisitedPageHandlerRemote, MostVisitedTheme, MostVisitedTile} from './most_visited.mojom-webui.js'; |
| import {MostVisitedWindowProxy} from './window_proxy.js'; |
| |
| enum ScreenWidth { |
| NARROW = 0, |
| MEDIUM = 1, |
| WIDE = 2, |
| } |
| |
| function resetTilePosition(tile: HTMLElement) { |
| tile.style.position = ''; |
| tile.style.left = ''; |
| tile.style.top = ''; |
| } |
| |
| function setTilePosition(tile: HTMLElement, {x, y}: {x: number, y: number}) { |
| tile.style.position = 'fixed'; |
| tile.style.left = `${x}px`; |
| tile.style.top = `${y}px`; |
| } |
| |
| function getHitIndex(rects: DOMRect[], x: number, y: number): number { |
| return rects.findIndex( |
| r => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom); |
| } |
| |
| |
| /** |
| * Returns null if URL is not valid. |
| */ |
| function normalizeUrl(urlString: string): URL|null { |
| try { |
| const url = new URL( |
| urlString.includes('://') ? urlString : `https://${urlString}/`); |
| if (['http:', 'https:'].includes(url.protocol)) { |
| return url; |
| } |
| } catch (e) { |
| } |
| return null; |
| } |
| |
| const MostVisitedElementBase = I18nMixin(PolymerElement); |
| |
| export interface MostVisitedElement { |
| $: { |
| actionMenu: CrActionMenuElement, |
| container: HTMLElement, |
| dialog: CrDialogElement, |
| toast: CrToastElement, |
| addShortcut: HTMLElement, |
| tiles: DomRepeat, |
| }; |
| } |
| |
| export class MostVisitedElement extends MostVisitedElementBase { |
| static get is() { |
| return 'cr-most-visited'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| theme: Object, |
| |
| /** |
| * When the tile icon background is dark, the icon color is white for |
| * contrast. This can be used to determine the color of the tile hover as |
| * well. |
| */ |
| useWhiteTileIcon_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| computed: `computeUseWhiteTileIcon_(theme)`, |
| }, |
| |
| /** |
| * If true wraps the tile titles in white pills. |
| */ |
| useTitlePill_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| computed: `computeUseTitlePill_(theme)`, |
| }, |
| |
| columnCount_: { |
| type: Number, |
| computed: `computeColumnCount_(tiles_, screenWidth_, maxTiles_)`, |
| }, |
| |
| rowCount_: { |
| type: Number, |
| computed: 'computeRowCount_(columnCount_, tiles_)', |
| }, |
| |
| customLinksEnabled_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| }, |
| |
| dialogTileTitle_: String, |
| |
| dialogTileUrl_: { |
| type: String, |
| observer: 'onDialogTileUrlChange_', |
| }, |
| |
| dialogTileUrlInvalid_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| dialogTitle_: String, |
| |
| dialogSaveDisabled_: { |
| type: Boolean, |
| computed: `computeDialogSaveDisabled_(dialogTitle_, dialogTileUrl_, |
| dialogShortcutAlreadyExists_)`, |
| }, |
| |
| dialogShortcutAlreadyExists_: { |
| type: Boolean, |
| computed: 'computeDialogShortcutAlreadyExists_(tiles_, dialogTileUrl_)', |
| }, |
| |
| dialogTileUrlError_: { |
| type: String, |
| computed: `computeDialogTileUrlError_(dialogTileUrl_, |
| dialogShortcutAlreadyExists_)`, |
| }, |
| |
| isDark_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| computed: `computeIsDark_(theme)`, |
| }, |
| |
| /** |
| * Used to hide hover style and cr-icon-button of tiles while the tiles |
| * are being reordered. |
| */ |
| reordering_: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| }, |
| |
| maxTiles_: { |
| type: Number, |
| computed: 'computeMaxTiles_(customLinksEnabled_)', |
| }, |
| |
| maxVisibleTiles_: { |
| type: Number, |
| computed: 'computeMaxVisibleTiles_(columnCount_, rowCount_)', |
| }, |
| |
| showAdd_: { |
| type: Boolean, |
| value: false, |
| computed: |
| 'computeShowAdd_(tiles_, maxVisibleTiles_, customLinksEnabled_)', |
| }, |
| |
| showToastButtons_: Boolean, |
| |
| screenWidth_: Number, |
| |
| tiles_: Array, |
| |
| toastContent_: String, |
| |
| visible_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| }, |
| }; |
| } |
| |
| private theme: MostVisitedTheme|null; |
| private useWhiteTileIcon_: boolean; |
| private useTitlePill_: boolean; |
| private columnCount_: number; |
| private rowCount_: number; |
| private customLinksEnabled_: boolean; |
| private dialogTileTitle_: string; |
| private dialogTileUrl_: string; |
| private dialogTileUrlInvalid_: boolean; |
| private dialogTitle_: string; |
| private dialogSaveDisabled_: boolean; |
| private dialogShortcutAlreadyExists_: boolean; |
| private dialogTileUrlError_: string; |
| private isDark_: boolean; |
| private reordering_: boolean; |
| private maxTiles_: number; |
| private maxVisibleTiles_: number; |
| private showAdd_: boolean; |
| private showToastButtons_: boolean; |
| private screenWidth_: ScreenWidth; |
| private tiles_: MostVisitedTile[]; |
| private toastContent_: string; |
| private visible_: boolean; |
| |
| private adding_: boolean = false; |
| private callbackRouter_: MostVisitedPageCallbackRouter; |
| private pageHandler_: MostVisitedPageHandlerRemote; |
| private windowProxy_: MostVisitedWindowProxy; |
| private setMostVisitedInfoListenerId_: number|null = null; |
| private actionMenuTargetIndex_: number = -1; |
| private dragOffset_: {x: number, y: number}|null; |
| private tileRects_: DOMRect[] = []; |
| private isRtl_: boolean; |
| private eventTracker_: EventTracker; |
| private boundOnWidthChange_: () => void; |
| private mediaListenerWideWidth_: MediaQueryList; |
| private mediaListenerMediumWidth_: MediaQueryList; |
| private boundOnDocumentKeyDown_: (e: KeyboardEvent) => void; |
| |
| private get tileElements_() { |
| return Array.from( |
| this.shadowRoot!.querySelectorAll<HTMLElement>('.tile:not([hidden])')); |
| } |
| |
| // Suppress TypeScript's error TS2376 to intentionally allow calling |
| // performance.mark() before calling super(). |
| // @ts-ignore |
| constructor() { |
| performance.mark('most-visited-creation-start'); |
| super(); |
| |
| this.callbackRouter_ = MostVisitedBrowserProxy.getInstance().callbackRouter; |
| |
| this.pageHandler_ = MostVisitedBrowserProxy.getInstance().handler; |
| |
| this.windowProxy_ = MostVisitedWindowProxy.getInstance(); |
| |
| /** |
| * This is the position of the mouse with respect to the top-left corner |
| * of the tile being dragged. |
| */ |
| this.dragOffset_ = null; |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| this.isRtl_ = window.getComputedStyle(this)['direction'] === 'rtl'; |
| this.eventTracker_ = new EventTracker(); |
| |
| this.setMostVisitedInfoListenerId_ = |
| this.callbackRouter_.setMostVisitedInfo.addListener( |
| (info: MostVisitedInfo) => { |
| performance.measure( |
| 'most-visited-mojo', 'most-visited-mojo-start'); |
| this.visible_ = info.visible; |
| this.customLinksEnabled_ = info.customLinksEnabled; |
| this.tiles_ = info.tiles.slice(0, assert(this.maxTiles_)); |
| }); |
| performance.mark('most-visited-mojo-start'); |
| this.eventTracker_.add(document, 'visibilitychange', () => { |
| // This updates the most visited tiles every time the NTP tab gets |
| // activated. |
| if (document.visibilityState === 'visible') { |
| this.pageHandler_.updateMostVisitedInfo(); |
| } |
| }); |
| this.pageHandler_.updateMostVisitedInfo(); |
| FocusOutlineManager.forDocument(document); |
| } |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| this.mediaListenerWideWidth_.removeListener( |
| assert(this.boundOnWidthChange_)); |
| this.mediaListenerMediumWidth_.removeListener( |
| assert(this.boundOnWidthChange_)); |
| this.ownerDocument.removeEventListener( |
| 'keydown', this.boundOnDocumentKeyDown_); |
| this.eventTracker_.removeAll(); |
| } |
| |
| override ready() { |
| super.ready(); |
| |
| this.boundOnWidthChange_ = this.updateScreenWidth_.bind(this); |
| this.mediaListenerWideWidth_ = |
| this.windowProxy_.matchMedia('(min-width: 672px)'); |
| this.mediaListenerWideWidth_.addListener(this.boundOnWidthChange_); |
| this.mediaListenerMediumWidth_ = |
| this.windowProxy_.matchMedia('(min-width: 560px)'); |
| this.mediaListenerMediumWidth_.addListener(this.boundOnWidthChange_); |
| this.updateScreenWidth_(); |
| this.boundOnDocumentKeyDown_ = e => this.onDocumentKeyDown_(e); |
| this.ownerDocument.addEventListener( |
| 'keydown', this.boundOnDocumentKeyDown_); |
| |
| performance.measure('most-visited-creation', 'most-visited-creation-start'); |
| } |
| |
| private rgbaOrInherit_(skColor: SkColor|null): string { |
| return skColor ? skColorToRgba(skColor) : 'inherit'; |
| } |
| |
| private clearForceHover_() { |
| const forceHover = this.shadowRoot!.querySelector('.force-hover'); |
| if (forceHover) { |
| forceHover.classList.remove('force-hover'); |
| } |
| } |
| |
| private computeColumnCount_(): number { |
| let maxColumns = 3; |
| if (this.screenWidth_ === ScreenWidth.WIDE) { |
| maxColumns = 5; |
| } else if (this.screenWidth_ === ScreenWidth.MEDIUM) { |
| maxColumns = 4; |
| } |
| |
| const shortcutCount = this.tiles_ ? this.tiles_.length : 0; |
| const canShowAdd = this.maxTiles_ > shortcutCount; |
| const tileCount = |
| Math.min(this.maxTiles_, shortcutCount + (canShowAdd ? 1 : 0)); |
| const columnCount = tileCount <= maxColumns ? |
| tileCount : |
| Math.min(maxColumns, Math.ceil(tileCount / 2)); |
| return columnCount || 3; |
| } |
| |
| private computeRowCount_(): number { |
| if (this.columnCount_ === 0) { |
| return 0; |
| } |
| |
| const shortcutCount = this.tiles_ ? this.tiles_.length : 0; |
| return this.columnCount_ <= shortcutCount ? 2 : 1; |
| } |
| |
| private computeMaxTiles_(): number { |
| return this.customLinksEnabled_ ? 10 : 8; |
| } |
| |
| private computeMaxVisibleTiles_(): number { |
| return this.columnCount_ * this.rowCount_; |
| } |
| |
| private computeShowAdd_(): boolean { |
| return this.customLinksEnabled_ && this.tiles_ && |
| this.tiles_.length < this.maxVisibleTiles_; |
| } |
| |
| private computeDialogSaveDisabled_(): boolean { |
| return !this.dialogTileUrl_.trim() || |
| normalizeUrl(this.dialogTileUrl_) === null || |
| this.dialogShortcutAlreadyExists_; |
| } |
| |
| private computeDialogShortcutAlreadyExists_(): boolean { |
| const dialogTileHref = (normalizeUrl(this.dialogTileUrl_) || {}).href; |
| if (!dialogTileHref) { |
| return false; |
| } |
| return (this.tiles_ || []).some(({url: {url}}, index) => { |
| if (index === this.actionMenuTargetIndex_) { |
| return false; |
| } |
| const otherUrl = normalizeUrl(url); |
| return otherUrl && otherUrl.href === dialogTileHref; |
| }); |
| } |
| |
| private computeDialogTileUrlError_(): string { |
| return loadTimeData.getString( |
| this.dialogShortcutAlreadyExists_ ? 'shortcutAlreadyExists' : |
| 'invalidUrl'); |
| } |
| |
| private computeIsDark_(): boolean { |
| return this.theme ? this.theme.isDark : false; |
| } |
| |
| private computeUseWhiteTileIcon_(): boolean { |
| return this.theme ? this.theme.useWhiteTileIcon : false; |
| } |
| |
| private computeUseTitlePill_(): boolean { |
| return this.theme ? this.theme.useTitlePill : false; |
| } |
| |
| /** |
| * If a pointer is over a tile rect that is different from the one being |
| * dragged, the dragging tile is moved to the new position. The reordering |
| * is done in the DOM and the by the |reorderMostVisitedTile()| call. This is |
| * done to prevent flicking between the time when the tiles are moved back to |
| * their original positions (by removing position absolute) and when the |
| * tiles are updated via a |setMostVisitedTiles()| call. |
| * |
| * |reordering_| is not set to false when the tiles are reordered. The callers |
| * will need to set it to false. This is necessary to handle a mouse drag |
| * issue. |
| */ |
| private dragEnd_(x: number, y: number) { |
| if (!this.customLinksEnabled_) { |
| this.reordering_ = false; |
| return; |
| } |
| this.dragOffset_ = null; |
| const dragElement = |
| this.shadowRoot!.querySelector<HTMLElement>('.tile.dragging'); |
| if (!dragElement) { |
| this.reordering_ = false; |
| return; |
| } |
| const dragIndex = (this.$.tiles.modelForElement(dragElement) as unknown as { |
| index: number, |
| }).index; |
| dragElement.classList.remove('dragging'); |
| this.tileElements_.forEach(el => resetTilePosition(el)); |
| resetTilePosition(this.$.addShortcut); |
| const dropIndex = getHitIndex(this.tileRects_, x, y); |
| if (dragIndex !== dropIndex && dropIndex > -1) { |
| const [draggingTile] = this.tiles_.splice(dragIndex, 1); |
| this.tiles_.splice(dropIndex, 0, draggingTile); |
| this.notifySplices('tiles_', [ |
| { |
| index: dragIndex, |
| removed: [draggingTile], |
| addedCount: 0, |
| object: this.tiles_, |
| type: 'splice', |
| }, |
| { |
| index: dropIndex, |
| removed: [], |
| addedCount: 1, |
| object: this.tiles_, |
| type: 'splice', |
| }, |
| ]); |
| this.pageHandler_.reorderMostVisitedTile(draggingTile.url, dropIndex); |
| } |
| } |
| |
| /** |
| * The positions of the tiles are updated based on the location of the |
| * pointer. |
| */ |
| private dragOver_(x: number, y: number) { |
| const dragElement = |
| this.shadowRoot!.querySelector<HTMLElement>('.tile.dragging'); |
| if (!dragElement) { |
| this.reordering_ = false; |
| return; |
| } |
| const dragIndex = (this.$.tiles.modelForElement(dragElement) as unknown as { |
| index: number, |
| }).index; |
| setTilePosition(dragElement, { |
| x: x - this.dragOffset_!.x, |
| y: y - this.dragOffset_!.y, |
| }); |
| const dropIndex = getHitIndex(this.tileRects_, x, y); |
| this.tileElements_.forEach((element, i) => { |
| let positionIndex; |
| if (i === dragIndex) { |
| return; |
| } else if (dropIndex === -1) { |
| positionIndex = i; |
| } else if (dragIndex < dropIndex && dragIndex <= i && i <= dropIndex) { |
| positionIndex = i - 1; |
| } else if (dragIndex > dropIndex && dragIndex >= i && i >= dropIndex) { |
| positionIndex = i + 1; |
| } else { |
| positionIndex = i; |
| } |
| setTilePosition(element, this.tileRects_[positionIndex]); |
| }); |
| } |
| |
| /** |
| * Sets up tile reordering for both drag and touch events. This method stores |
| * the following to be used in |dragOver_()| and |dragEnd_()|. |
| * |dragOffset_|: This is the mouse/touch offset with respect to the |
| * top/left corner of the tile being dragged. It is used to update the |
| * dragging tile location during the drag. |
| * |reordering_|: This is property/attribute used to hide the hover style |
| * and cr-icon-button of the tiles while they are being reordered. |
| * |tileRects_|: This is the rects of the tiles before the drag start. It is |
| * to determine which tile the pointer is over while dragging. |
| */ |
| private dragStart_(dragElement: HTMLElement, x: number, y: number) { |
| // Need to clear the tile that has a forced hover style for when the drag |
| // started without moving the mouse after the last drag/drop. |
| this.clearForceHover_(); |
| |
| dragElement.classList.add('dragging'); |
| const dragElementRect = dragElement.getBoundingClientRect(); |
| this.dragOffset_ = { |
| x: x - dragElementRect.x, |
| y: y - dragElementRect.y, |
| }; |
| const tileElements = this.tileElements_; |
| // Get all the rects first before setting the absolute positions. |
| this.tileRects_ = tileElements.map(t => t.getBoundingClientRect()); |
| if (this.showAdd_) { |
| const element = this.$.addShortcut; |
| setTilePosition(element, element.getBoundingClientRect()); |
| } |
| tileElements.forEach((tile, i) => { |
| setTilePosition(tile, this.tileRects_[i]); |
| }); |
| this.reordering_ = true; |
| } |
| |
| private getFaviconUrl_(url: Url): string { |
| const faviconUrl = new URL('chrome://favicon2/'); |
| faviconUrl.searchParams.set('size', '24'); |
| faviconUrl.searchParams.set('scaleFactor', '1x'); |
| faviconUrl.searchParams.set('showFallbackMonogram', ''); |
| faviconUrl.searchParams.set('pageUrl', url.url); |
| return faviconUrl.href; |
| } |
| |
| private getRestoreButtonText_(): string { |
| return loadTimeData.getString( |
| this.customLinksEnabled_ ? 'restoreDefaultLinks' : |
| 'restoreThumbnailsShort'); |
| } |
| |
| private getTileTitleDirectionClass_(tile: MostVisitedTile): string { |
| return tile.titleDirection === TextDirection.RIGHT_TO_LEFT ? 'title-rtl' : |
| 'title-ltr'; |
| } |
| |
| private isHidden_(index: number): boolean { |
| return index >= this.maxVisibleTiles_; |
| } |
| |
| private onAdd_() { |
| this.dialogTitle_ = loadTimeData.getString('addLinkTitle'); |
| this.dialogTileTitle_ = ''; |
| this.dialogTileUrl_ = ''; |
| this.dialogTileUrlInvalid_ = false; |
| this.adding_ = true; |
| this.$.dialog.showModal(); |
| } |
| |
| private onAddShortcutKeyDown_(e: KeyboardEvent) { |
| if (hasKeyModifiers(e)) { |
| return; |
| } |
| |
| if (!this.tiles_ || this.tiles_.length === 0) { |
| return; |
| } |
| const backKey = this.isRtl_ ? 'ArrowRight' : 'ArrowLeft'; |
| if (e.key === backKey || e.key === 'ArrowUp') { |
| this.tileFocus_(this.tiles_.length - 1); |
| } |
| } |
| |
| private onDialogCancel_() { |
| this.actionMenuTargetIndex_ = -1; |
| this.$.dialog.cancel(); |
| } |
| |
| private onDialogClose_() { |
| this.dialogTileUrl_ = ''; |
| if (this.adding_) { |
| this.$.addShortcut.focus(); |
| } |
| this.adding_ = false; |
| } |
| |
| private onDialogTileUrlBlur_() { |
| if (this.dialogTileUrl_.length > 0 && |
| (normalizeUrl(this.dialogTileUrl_) === null || |
| this.dialogShortcutAlreadyExists_)) { |
| this.dialogTileUrlInvalid_ = true; |
| } |
| } |
| |
| private onDialogTileUrlChange_() { |
| this.dialogTileUrlInvalid_ = false; |
| } |
| |
| private onDocumentKeyDown_(e: KeyboardEvent) { |
| if (e.altKey || e.shiftKey) { |
| return; |
| } |
| |
| const modifier = isMac ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey; |
| if (modifier && e.key === 'z') { |
| e.preventDefault(); |
| this.onUndoClick_(); |
| } |
| } |
| |
| private onDragStart_(e: DragEvent) { |
| if (!this.customLinksEnabled_) { |
| return; |
| } |
| // |dataTransfer| is null in tests. |
| if (e.dataTransfer) { |
| // Remove the ghost image that appears when dragging. |
| e.dataTransfer.setDragImage(new Image(), 0, 0); |
| } |
| |
| this.dragStart_(e.target as HTMLElement, e.x, e.y); |
| const dragOver = (e: DragEvent) => { |
| e.preventDefault(); |
| e.dataTransfer!.dropEffect = 'move'; |
| this.dragOver_(e.x, e.y); |
| }; |
| this.ownerDocument.addEventListener('dragover', dragOver); |
| this.ownerDocument.addEventListener('dragend', e => { |
| this.ownerDocument.removeEventListener('dragover', dragOver); |
| this.dragEnd_(e.x, e.y); |
| const dropIndex = getHitIndex(this.tileRects_, e.x, e.y); |
| if (dropIndex !== -1) { |
| this.tileElements_[dropIndex].classList.add('force-hover'); |
| } |
| this.addEventListener('pointermove', () => { |
| this.clearForceHover_(); |
| // When |reordering_| is true, the normal hover style is not shown. |
| // After a drop, the element that has hover is not correct. It will be |
| // after the mouse moves. |
| this.reordering_ = false; |
| }, {once: true}); |
| }, {once: true}); |
| } |
| |
| private onEdit_() { |
| this.$.actionMenu.close(); |
| this.dialogTitle_ = loadTimeData.getString('editLinkTitle'); |
| const tile = this.tiles_[this.actionMenuTargetIndex_]; |
| this.dialogTileTitle_ = tile.title; |
| this.dialogTileUrl_ = tile.url.url; |
| this.dialogTileUrlInvalid_ = false; |
| this.$.dialog.showModal(); |
| } |
| |
| private onRestoreDefaultsClick_() { |
| if (!this.$.toast.open || !this.showToastButtons_) { |
| return; |
| } |
| this.$.toast.hide(); |
| this.pageHandler_.restoreMostVisitedDefaults(); |
| } |
| |
| private async onRemove_() { |
| this.$.actionMenu.close(); |
| await this.tileRemove_(this.actionMenuTargetIndex_); |
| this.actionMenuTargetIndex_ = -1; |
| } |
| |
| private async onSave_() { |
| const newUrl = {url: normalizeUrl(this.dialogTileUrl_)!.href}; |
| this.$.dialog.close(); |
| let newTitle = this.dialogTileTitle_.trim(); |
| if (newTitle.length === 0) { |
| newTitle = this.dialogTileUrl_; |
| } |
| if (this.adding_) { |
| const {success} = |
| await this.pageHandler_.addMostVisitedTile(newUrl, newTitle); |
| this.toast_(success ? 'linkAddedMsg' : 'linkCantCreate', success); |
| } else { |
| const {url, title} = this.tiles_[this.actionMenuTargetIndex_]; |
| if (url.url !== newUrl.url || title !== newTitle) { |
| const {success} = await this.pageHandler_.updateMostVisitedTile( |
| url, newUrl, newTitle); |
| this.toast_(success ? 'linkEditedMsg' : 'linkCantEdit', success); |
| } |
| this.actionMenuTargetIndex_ = -1; |
| } |
| } |
| |
| private onTileActionButtonClick_(e: DomRepeatEvent<MostVisitedTile>) { |
| e.preventDefault(); |
| this.actionMenuTargetIndex_ = e.model.index; |
| this.$.actionMenu.showAt(e.target as HTMLElement); |
| } |
| |
| private onTileRemoveButtonClick_(e: DomRepeatEvent<MostVisitedTile>) { |
| e.preventDefault(); |
| this.tileRemove_(e.model.index); |
| } |
| |
| private onTileClick_(e: DomRepeatEvent<MostVisitedTile, MouseEvent>) { |
| if (e.defaultPrevented) { |
| // Ignore previousely handled events. |
| return; |
| } |
| |
| if (loadTimeData.getBoolean('handleMostVisitedNavigationExplicitly')) { |
| e.preventDefault(); // Prevents default browser action (navigation). |
| } |
| |
| this.pageHandler_.onMostVisitedTileNavigation( |
| e.model.item, e.model.index, e.button || 0, e.altKey, e.ctrlKey, |
| e.metaKey, e.shiftKey); |
| } |
| |
| private onTileKeyDown_(e: DomRepeatEvent<MostVisitedTile, KeyboardEvent>) { |
| if (hasKeyModifiers(e)) { |
| return; |
| } |
| |
| if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && |
| e.key !== 'ArrowUp' && e.key !== 'ArrowDown' && e.key !== 'Delete') { |
| return; |
| } |
| |
| const index = e.model.index; |
| if (e.key === 'Delete') { |
| this.tileRemove_(index); |
| return; |
| } |
| |
| const advanceKey = this.isRtl_ ? 'ArrowLeft' : 'ArrowRight'; |
| const delta = (e.key === advanceKey || e.key === 'ArrowDown') ? 1 : -1; |
| this.tileFocus_(Math.max(0, index + delta)); |
| } |
| |
| private onUndoClick_() { |
| if (!this.$.toast.open || !this.showToastButtons_) { |
| return; |
| } |
| this.$.toast.hide(); |
| this.pageHandler_.undoMostVisitedTileAction(); |
| } |
| |
| private onTouchStart_(e: TouchEvent) { |
| if (this.reordering_ || !this.customLinksEnabled_) { |
| return; |
| } |
| const tileElement = |
| (e.composedPath() as HTMLElement[]) |
| .find(el => el.classList && el.classList.contains('tile')); |
| if (!tileElement) { |
| return; |
| } |
| const {clientX, clientY} = e.changedTouches[0]; |
| this.dragStart_(tileElement, clientX, clientY); |
| const touchMove = (e: TouchEvent) => { |
| const {clientX, clientY} = e.changedTouches[0]; |
| this.dragOver_(clientX, clientY); |
| }; |
| const touchEnd = (e: TouchEvent) => { |
| this.ownerDocument.removeEventListener('touchmove', touchMove); |
| tileElement.removeEventListener('touchend', touchEnd); |
| tileElement.removeEventListener('touchcancel', touchEnd); |
| const {clientX, clientY} = e.changedTouches[0]; |
| this.dragEnd_(clientX, clientY); |
| this.reordering_ = false; |
| }; |
| this.ownerDocument.addEventListener('touchmove', touchMove); |
| tileElement.addEventListener('touchend', touchEnd, {once: true}); |
| tileElement.addEventListener('touchcancel', touchEnd, {once: true}); |
| } |
| |
| private tileFocus_(index: number) { |
| if (index < 0) { |
| return; |
| } |
| const tileElements = this.tileElements_; |
| if (index < tileElements.length) { |
| tileElements[index].focus(); |
| } else if (this.showAdd_ && index === tileElements.length) { |
| this.$.addShortcut.focus(); |
| } |
| } |
| |
| private toast_(msgId: string, showButtons: boolean) { |
| this.toastContent_ = loadTimeData.getString(msgId); |
| this.showToastButtons_ = showButtons; |
| this.$.toast.show(); |
| } |
| |
| private tileRemove_(index: number) { |
| const {url, isQueryTile} = this.tiles_[index]; |
| this.pageHandler_.deleteMostVisitedTile(url); |
| // Do not show the toast buttons when a query tile is removed unless it is a |
| // custom link. Removal is not reversible for non custom link query tiles. |
| this.toast_( |
| 'linkRemovedMsg', |
| /* showButtons= */ this.customLinksEnabled_ || !isQueryTile); |
| this.tileFocus_(index); |
| } |
| |
| private updateScreenWidth_() { |
| if (this.mediaListenerWideWidth_.matches) { |
| this.screenWidth_ = ScreenWidth.WIDE; |
| } else if (this.mediaListenerMediumWidth_.matches) { |
| this.screenWidth_ = ScreenWidth.MEDIUM; |
| } else { |
| this.screenWidth_ = ScreenWidth.NARROW; |
| } |
| } |
| |
| private onTilesRendered_() { |
| performance.measure('most-visited-rendered'); |
| this.pageHandler_.onMostVisitedTilesRendered( |
| this.tiles_.slice(0, assert(this.maxVisibleTiles_)), |
| this.windowProxy_.now()); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'cr-most-visited': MostVisitedElement; |
| } |
| } |
| |
| customElements.define(MostVisitedElement.is, MostVisitedElement); |