blob: 485222881fb8494511e0aea79cc5ed0a6263949f [file] [log] [blame]
/* Copyright 2018 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. */
/**
* Enum for ids.
* @enum {string}
* @const
*/
const IDS = {
CANCEL: 'cancel', // Cancel button.
DELETE: 'delete', // Delete button.
DIALOG_TITLE: 'dialog-title', // Dialog title.
DONE: 'done', // Done button.
EDIT_DIALOG: 'edit-link-dialog', // Dialog element.
FORM: 'edit-form', // The edit link form.
INVALID_URL: 'invalid-url', // Invalid URL error message.
TITLE_FIELD: 'title-field', // Title input field.
TITLE_FIELD_NAME: 'title-field-name', // Title input field name.
URL_FIELD: 'url-field', // URL input field.
URL_FIELD_CONTAINER: 'url', // URL input field container.
URL_FIELD_NAME: 'url-field-name', // URL input field name.
};
/**
* Enum for key codes.
* @enum {number}
* @const
*/
const KEYCODES = {
ENTER: 13,
ESC: 27,
SPACE: 32,
TAB: 9,
};
/**
* The origin of this request, i.e. 'https://www.google.TLD' for the remote NTP,
* or 'chrome-search://local-ntp' for the local NTP.
* @const {string}
*/
const DOMAIN_ORIGIN = '{{ORIGIN}}';
/**
* List of parameters passed by query args.
* @type {Object}
*/
let queryArgs = {};
/**
* The prepopulated data for the form. Includes title, url, and rid.
* @type {Object}
*/
let prepopulatedLink = {
rid: -1,
title: '',
url: '',
};
/**
* The title of the dialog when adding a link.
* @type {string}
*/
let addLinkTitle = '';
/**
* The title of the dialog when editing a link.
* @type {string}
*/
let editLinkTitle = '';
/**
* The accessibility title of remove link button.
* @type {string}
*/
let deleteLinkTitle = '';
/**
* Handler for the 'linkData' message from the host page. Pre-populates the url
* and title fields with link's data obtained using the rid. Called if we are
* editing an existing link.
* @param {number} rid Restricted id of the link to be edited.
*/
function prepopulateFields(rid) {
if (!isFinite(rid)) {
return;
}
// Grab the link data from the embeddedSearch API.
let data = chrome.embeddedSearch.newTabPage.getMostVisitedItemData(rid);
if (!data) {
return;
}
prepopulatedLink.rid = rid;
$(IDS.TITLE_FIELD).value = prepopulatedLink.title = data.title;
$(IDS.TITLE_FIELD).dir = data.direction || 'ltr';
$(IDS.URL_FIELD).value = prepopulatedLink.url = data.url;
// Set accessibility names.
$(IDS.DELETE).setAttribute('aria-label', deleteLinkTitle + ' ' + data.title);
$(IDS.DONE).setAttribute('aria-label', editLinkTitle + ' ' + data.title);
$(IDS.DONE).title = editLinkTitle;
}
/**
* Shows the invalid URL error message until the URL field is modified.
*/
function showInvalidUrlUntilTextInput() {
$(IDS.URL_FIELD_CONTAINER).classList.add('invalid');
let reenable = (event) => {
$(IDS.URL_FIELD_CONTAINER).classList.remove('invalid');
$(IDS.URL_FIELD).removeEventListener('input', reenable);
};
$(IDS.URL_FIELD).addEventListener('input', reenable);
}
/**
* Send a message to close the edit dialog. Called when the edit flow has been
* completed. If the fields were unchanged, does not update the link data.
*/
function finishEditLink() {
let newUrl = '';
let newTitle = '';
const urlValue = $(IDS.URL_FIELD).value;
if (urlValue != prepopulatedLink.url) {
newUrl = chrome.embeddedSearch.newTabPage.fixupAndValidateUrl(urlValue);
// Show error message for invalid urls.
if (!newUrl || (newUrl && !utils.isSchemeAllowed(newUrl))) {
showInvalidUrlUntilTextInput();
$(IDS.DONE).disabled = true; // Disable submit until text input.
return;
}
}
const titleValue = $(IDS.TITLE_FIELD).value;
if (!titleValue) { // Set the URL input as the title if no title is provided.
newTitle = urlValue;
} else if (titleValue != prepopulatedLink.title) {
newTitle = titleValue;
}
// Update the link only if a field was changed.
if (!!newUrl || !!newTitle) {
chrome.embeddedSearch.newTabPage.updateCustomLink(
prepopulatedLink.rid, newUrl, newTitle);
}
closeDialog();
}
/**
* Call the EmbeddedSearchAPI to delete the link. Closes the dialog.
* @param {!Event} event The click event.
*/
function deleteLink(event) {
chrome.embeddedSearch.newTabPage.deleteMostVisitedItem(prepopulatedLink.rid);
closeDialog();
}
/**
* Send a message to close the edit dialog, clears the url and title fields, and
* resets the button statuses. Called when the edit flow has been completed.
*/
function closeDialog() {
window.parent.postMessage({cmd: 'closeDialog'}, DOMAIN_ORIGIN);
// Small delay to allow the dialog to close before cleaning up.
window.setTimeout(() => {
$(IDS.FORM).reset();
$(IDS.TITLE_FIELD).dir = null;
$(IDS.URL_FIELD_CONTAINER).classList.remove('invalid');
$(IDS.DELETE).disabled = false;
$(IDS.DONE).disabled = false;
prepopulatedLink.rid = -1;
prepopulatedLink.title = '';
prepopulatedLink.url = '';
}, 10);
}
/**
* Send a message to refocus the edited tile's three dot menu or the add
* shortcut tile after the cancel button is clicked.
* @param {Event} event The keydown event
*/
function focusBackOnCancel(event) {
if (event.keyCode === KEYCODES.ENTER || event.keyCode === KEYCODES.SPACE) {
let message = {
cmd: 'focusMenu',
tid: prepopulatedLink.rid
};
window.parent.postMessage(message, DOMAIN_ORIGIN);
event.preventDefault();
closeDialog();
}
}
/**
* Handler for the 'updateTheme' message from the host page.
* @param {object} info Data received in the message.
*/
function updateTheme(info) {
document.documentElement.setAttribute('darkmode', info.isDarkMode);
}
/**
* Event handler for messages from the host page.
* @param {Event} event Event received.
*/
function handlePostMessage(event) {
let cmd = event.data.cmd;
let args = event.data;
if (cmd === 'linkData') {
if (args.tid) { // We are editing a link, prepopulate the link data.
document.title = editLinkTitle;
$(IDS.DIALOG_TITLE).textContent = editLinkTitle;
prepopulateFields(args.tid);
} else { // We are adding a link, disable the delete button.
document.title = addLinkTitle;
$(IDS.DIALOG_TITLE).textContent = addLinkTitle;
$(IDS.DELETE).disabled = true;
$(IDS.DONE).disabled = true;
// Set accessibility names.
$(IDS.DONE).setAttribute('aria-label', addLinkTitle);
$(IDS.DONE).title = addLinkTitle;
}
// Timeout is required to allow the iframe to become visible before focusing
// the first input field.
window.setTimeout(() => {
$(IDS.TITLE_FIELD).select();
}, 10);
} else if (cmd === 'updateTheme') {
updateTheme(args);
}
}
/**
* Does some initialization and shows the dialog window.
*/
function init() {
// Parse query arguments.
let query = window.location.search.substring(1).split('&');
queryArgs = {};
for (let i = 0; i < query.length; ++i) {
let val = query[i].split('=');
if (val[0] == '') {
continue;
}
queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]);
}
document.title = queryArgs['editTitle'];
// Enable RTL.
if (queryArgs['rtl'] == '1') {
document.documentElement.setAttribute('dir', 'rtl');
}
// Populate text content.
addLinkTitle = queryArgs['addTitle'];
editLinkTitle = queryArgs['editTitle'];
deleteLinkTitle = queryArgs['linkRemove'];
$(IDS.DIALOG_TITLE).textContent = addLinkTitle;
$(IDS.TITLE_FIELD_NAME).textContent = queryArgs['nameField'];
$(IDS.TITLE_FIELD_NAME).setAttribute('aria-label', queryArgs['nameField']);
$(IDS.URL_FIELD_NAME).textContent = queryArgs['urlField'];
$(IDS.URL_FIELD_NAME).setAttribute('aria-label', queryArgs['urlField']);
$(IDS.DELETE).textContent = $(IDS.DELETE).title = queryArgs['linkRemove'];
$(IDS.CANCEL).textContent = $(IDS.CANCEL).title = queryArgs['linkCancel'];
$(IDS.CANCEL).setAttribute('aria-label', queryArgs['linkCancel']);
$(IDS.DONE).textContent = $(IDS.DONE).title = queryArgs['linkDone'];
$(IDS.INVALID_URL).textContent = queryArgs['invalidUrl'];
// Set up event listeners.
document.body.onkeydown = (event) => {
if (event.keyCode === KEYCODES.ESC) {
// Close the iframe instead of just this dialog.
event.preventDefault();
closeDialog();
}
};
$(IDS.DELETE).addEventListener('click', deleteLink);
$(IDS.CANCEL).addEventListener('click', closeDialog);
$(IDS.CANCEL).addEventListener('keydown', focusBackOnCancel);
$(IDS.FORM).addEventListener('submit', (event) => {
// Prevent the form from submitting and modifying the URL.
event.preventDefault();
finishEditLink();
});
let finishEditOrClose = (event) => {
if (event.keyCode === KEYCODES.ENTER) {
event.preventDefault();
if (!$(IDS.DONE).disabled) {
finishEditLink();
}
}
};
$(IDS.TITLE_FIELD).onkeydown = finishEditOrClose;
$(IDS.URL_FIELD).onkeydown = finishEditOrClose;
utils.disableOutlineOnMouseClick($(IDS.DELETE));
utils.disableOutlineOnMouseClick($(IDS.CANCEL));
utils.disableOutlineOnMouseClick($(IDS.DONE));
animations.addRippleAnimations();
// Change input field name to blue on input field focus.
let changeColor = (fieldTitle) => {
$(fieldTitle).classList.toggle('focused');
};
$(IDS.TITLE_FIELD)
.addEventListener('focusin', () => changeColor(IDS.TITLE_FIELD_NAME));
$(IDS.TITLE_FIELD)
.addEventListener('blur', () => changeColor(IDS.TITLE_FIELD_NAME));
$(IDS.URL_FIELD)
.addEventListener('focusin', () => changeColor(IDS.URL_FIELD_NAME));
$(IDS.URL_FIELD)
.addEventListener('blur', () => changeColor(IDS.URL_FIELD_NAME));
// Disables the "Done" button when the URL field is empty.
$(IDS.URL_FIELD).addEventListener('input',
() => $(IDS.DONE).disabled = ($(IDS.URL_FIELD).value.trim() === ''));
$(IDS.EDIT_DIALOG).showModal();
window.addEventListener('message', handlePostMessage);
}
window.addEventListener('DOMContentLoaded', init);