blob: abdc00ec6aaa7257666fa6cecc22e49500e4c64c [file] [log] [blame]
// Copyright 2020 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 {browserProxy} from './browser_proxy/browser_proxy.js';
import {assert} from './chrome_util.js';
import * as dom from './dom.js';
import * as snackbar from './snackbar.js';
import * as state from './state.js';
import {OneShotTimer} from './timer.js';
// TODO(b/172879638): Tune the duration according to the final motion spec.
const CHIP_DURATION = 8000;
/**
* The detected string that is being shown currently.
* @type {?string}
*/
let currentCode = null;
/**
* The barcode chip container that is being shown currently.
* @type {?HTMLElement}
*/
let currentChip = null;
/**
* The countdown timer for dismissing the chip.
* @type {?OneShotTimer}
*/
let currentTimer = null;
/**
* Resets the variables of the current state and dismisses the chip.
*/
function deactivate() {
if (currentChip !== null) {
currentChip.classList.add('invisible');
}
currentCode = null;
currentChip = null;
currentTimer = null;
}
/**
* Activates the chip on container and starts the timer.
* @param {!HTMLElement} container The container of the chip.
*/
function activate(container) {
container.classList.remove('invisible');
currentChip = container;
currentTimer = new OneShotTimer(deactivate, CHIP_DURATION);
if (state.get(state.State.TAB_NAVIGATION)) {
// Do not auto dismiss the chip when using keyboard for a11y. Screen reader
// might need long time to read the detected content.
currentTimer.stop();
}
}
/**
* Checks whether a string is a regular url link with http or https protocol.
* @param {string} s
* @return {boolean}
*/
function isSafeUrl(s) {
try {
const url = new URL(s);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
console.warn('Reject url with protocol:', url.protocol);
return false;
}
return true;
} catch (e) {
return false;
}
}
/**
* Setups the copy button.
* @param {!HTMLElement} container The container for the button.
* @param {string} content The content to be copied.
* @param {string} snackbarLabel The label to be displayed on snackbar when the
* content is copied.
* @return {!HTMLElement} The copy button element.
*/
function setupCopyButton(container, content, snackbarLabel) {
const copyButton =
dom.getFrom(container, '.barcode-copy-button', HTMLButtonElement);
copyButton.onclick = async () => {
await navigator.clipboard.writeText(content);
snackbar.show(snackbarLabel);
};
return copyButton;
}
/**
* Shows an actionable url chip.
* @param {string} url
*/
function showUrl(url) {
const container = dom.get('#barcode-chip-url-container', HTMLDivElement);
activate(container);
const anchor = dom.getFrom(container, 'a', HTMLAnchorElement);
Object.assign(anchor, {
href: url,
textContent: url,
});
const hostname = new URL(url).hostname;
const label = browserProxy.getI18nMessage('barcode_link_detected', hostname);
anchor.setAttribute('aria-label', label);
anchor.setAttribute('aria-description', url);
anchor.focus();
setupCopyButton(container, url, 'snackbar_link_copied');
}
/**
* Shows an actionable text chip.
* @param {string} text
*/
function showText(text) {
const container = dom.get('#barcode-chip-text-container', HTMLDivElement);
activate(container);
container.classList.remove('expanded');
const textEl = dom.get('#barcode-chip-text-content', HTMLDivElement);
textEl.textContent = text;
const expandable = textEl.scrollWidth > textEl.clientWidth;
const expandEl = dom.get('#barcode-chip-text-expand', HTMLButtonElement);
expandEl.classList.toggle('hidden', !expandable);
expandEl.onclick = () => {
container.classList.toggle('expanded');
const expanded = container.classList.contains('expanded');
expandEl.setAttribute('aria-expanded', expanded.toString());
};
const copyButton = setupCopyButton(container, text, 'snackbar_text_copied');
// TODO(b/172879638): There is a race in ChromeVox which will speak the
// focused element twice.
if (expandable) {
expandEl.focus();
} else {
copyButton.focus();
}
}
/**
* Shows an actionable chip for the string detected from a barcode.
* @param {string} code
*/
export async function show(code) {
if (code === currentCode) {
if (currentTimer !== null) {
// Extend the duration by resetting the timeout.
currentTimer.resetTimeout();
}
return;
}
if (currentTimer !== null) {
// Dismiss the previous chip.
currentTimer.fireNow();
assert(currentTimer === null, 'The timer should be cleared.');
}
currentCode = code;
if (isSafeUrl(code)) {
showUrl(code);
} else {
showText(code);
}
}