blob: 77b5c7ed09270e9e30caf211f556f5bad46de72f [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './viewer-thumbnail.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PluginController, PluginControllerEventType} from '../controller.js';
import {getTemplate} from './viewer-thumbnail-bar.html.js';
import {ViewerThumbnailElement} from './viewer-thumbnail.js';
export interface ViewerThumbnailBarElement {
$: {
thumbnails: HTMLElement,
};
}
export class ViewerThumbnailBarElement extends PolymerElement {
static get is() {
return 'viewer-thumbnail-bar';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
activePage: {
type: Number,
observer: 'activePageChanged_',
},
clockwiseRotations: Number,
docLength: Number,
isPluginActive_: Boolean,
pageNumbers_: {
type: Array,
computed: 'computePageNumbers_(docLength)',
},
};
}
activePage: number;
clockwiseRotations: number;
docLength: number;
private isPluginActive_: boolean;
private pageNumbers_: number[];
private intersectionObserver_: IntersectionObserver;
private pluginController_: PluginController = PluginController.getInstance();
private tracker_: EventTracker = new EventTracker();
// TODO(dhoss): Remove `this.inTest` when implemented a mock plugin
// controller.
inTest: boolean = false;
constructor() {
super();
this.isPluginActive_ = this.pluginController_.isActive;
// Listen to whether the plugin is active. Thumbnails should be hidden
// when the plugin is inactive.
this.tracker_.add(
this.pluginController_.getEventTarget(),
PluginControllerEventType.IS_ACTIVE_CHANGED,
(e: CustomEvent<boolean>) => this.isPluginActive_ = e.detail);
}
override ready() {
super.ready();
this.addEventListener('focus', this.onFocus_);
this.addEventListener('keydown', this.onKeydown_);
const thumbnailsDiv = this.$.thumbnails;
this.intersectionObserver_ =
new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
const thumbnail = entry.target as ViewerThumbnailElement;
if (!entry.isIntersecting) {
thumbnail.clearImage();
return;
}
if (thumbnail.isPainted()) {
return;
}
thumbnail.setPainted();
if (!this.isPluginActive_ || this.inTest) {
return;
}
this.pluginController_.requestThumbnail(thumbnail.pageNumber)
.then(response => {
const array = new Uint8ClampedArray(response.imageData);
const imageData = new ImageData(array, response.width);
thumbnail.image = imageData;
});
});
}, {
root: thumbnailsDiv,
// The root margin is set to 100% on the bottom to prepare thumbnails
// that are one standard scroll finger swipe away. The root margin is
// set to 500% on the top to discard thumbnails that are far from
// view, but to avoid regenerating thumbnails that are close.
rootMargin: '500% 0% 100%',
});
FocusOutlineManager.forDocument(document);
}
/**
* Changes the focus to the thumbnail of the new active page if the focus was
* already on a thumbnail.
*/
private activePageChanged_() {
if (this.shadowRoot!.activeElement) {
this.getThumbnailForPage(this.activePage)!.focusAndScroll();
}
}
private clickThumbnailForPage(pageNumber: number) {
const thumbnail = this.getThumbnailForPage(pageNumber);
if (!thumbnail) {
return;
}
thumbnail.getClickTarget().click();
}
getThumbnailForPage(pageNumber: number): ViewerThumbnailElement|null {
return this.shadowRoot!.querySelector(
`viewer-thumbnail:nth-child(${pageNumber})`);
}
/** @return The array of page numbers. */
private computePageNumbers_(): number[] {
return Array.from({length: this.docLength}, (_, i) => i + 1);
}
private getAriaLabel_(pageNumber: number): string {
return loadTimeData.getStringF('thumbnailPageAriaLabel', pageNumber);
}
/** @return Whether the page is the current page. */
private isActivePage_(page: number): boolean {
return this.activePage === page;
}
private onDomChange_() {
this.shadowRoot!.querySelectorAll('viewer-thumbnail').forEach(thumbnail => {
this.intersectionObserver_.observe(thumbnail);
});
}
/** Forwards focus to a thumbnail when tabbing. */
private onFocus_() {
// Ignore focus triggered by mouse to allow the focus to go straight to the
// thumbnail being clicked.
const focusOutlineManager = FocusOutlineManager.forDocument(document);
if (!focusOutlineManager.visible) {
return;
}
// Change focus to the thumbnail of the active page.
const activeThumbnail =
this.shadowRoot!.querySelector<ViewerThumbnailElement>(
'viewer-thumbnail[is-active]');
if (activeThumbnail) {
activeThumbnail.focus();
return;
}
// Otherwise change to the first thumbnail, if there is one.
const firstThumbnail = this.shadowRoot!.querySelector('viewer-thumbnail');
if (!firstThumbnail) {
return;
}
firstThumbnail.focus();
}
private onKeydown_(e: KeyboardEvent) {
switch (e.key) {
case 'Tab':
// On shift+tab, first redirect focus from the thumbnails to:
// 1) Avoid focusing on the thumbnail bar.
// 2) Focus to the element before the thumbnail bar from any thumbnail.
if (e.shiftKey) {
this.focus();
return;
}
// On tab, first redirect focus to the last thumbnail to focus to the
// element after the thumbnail bar from any thumbnail.
const lastThumbnail =
this.shadowRoot!.querySelector<ViewerThumbnailElement>(
'viewer-thumbnail:last-of-type');
assert(lastThumbnail);
lastThumbnail.focus({preventScroll: true});
break;
case 'ArrowRight':
case 'ArrowDown':
// Prevent default arrow scroll behavior.
e.preventDefault();
this.clickThumbnailForPage(this.activePage + 1);
break;
case 'ArrowLeft':
case 'ArrowUp':
// Prevent default arrow scroll behavior.
e.preventDefault();
this.clickThumbnailForPage(this.activePage - 1);
break;
}
}
}
declare global {
interface HTMLElementTagNameMap {
'viewer-thumbnail-bar': ViewerThumbnailBarElement;
}
}
customElements.define(ViewerThumbnailBarElement.is, ViewerThumbnailBarElement);