diff --git a/.eslintrc.json b/.eslintrc.json index cec9abc..4f0c7af 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,11 @@ { "env": { + "es2017": true, "browser": true }, "extends": "eslint-config-fluid", "rules": { - "indent": [ + "@stylistic/js/indent": [ "error", 4, { "SwitchCase": 1} diff --git a/AUTHORS.md b/AUTHORS.md index 88f9018..1f526ec 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,5 +1,5 @@ + + + + Gamepad Navigator Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Gamepad Navigator Settings

+
+ +
+
+ + diff --git a/src/images/gamepad-icon-128px.png b/src/images/gamepad-icon-128px.png index 708d83c..b9bd877 100644 Binary files a/src/images/gamepad-icon-128px.png and b/src/images/gamepad-icon-128px.png differ diff --git a/src/images/gamepad-icon-16px.png b/src/images/gamepad-icon-16px.png index 9217588..0d9a47f 100644 Binary files a/src/images/gamepad-icon-16px.png and b/src/images/gamepad-icon-16px.png differ diff --git a/src/images/gamepad-icon-32px.png b/src/images/gamepad-icon-32px.png index 1f64b5b..9e13892 100644 Binary files a/src/images/gamepad-icon-32px.png and b/src/images/gamepad-icon-32px.png differ diff --git a/src/images/gamepad-icon-48px.png b/src/images/gamepad-icon-48px.png index 3e19627..461f08c 100644 Binary files a/src/images/gamepad-icon-48px.png and b/src/images/gamepad-icon-48px.png differ diff --git a/src/images/gamepad-icon.png b/src/images/gamepad-icon.png index dc23b60..cce10ad 100644 Binary files a/src/images/gamepad-icon.png and b/src/images/gamepad-icon.png differ diff --git a/src/images/gamepad-icon.svg b/src/images/gamepad-icon.svg new file mode 100644 index 0000000..5ce8c92 --- /dev/null +++ b/src/images/gamepad-icon.svg @@ -0,0 +1,28 @@ + + + + diff --git a/src/images/thumbstick.svg b/src/images/thumbstick.svg new file mode 100644 index 0000000..719f9d6 --- /dev/null +++ b/src/images/thumbstick.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/src/js/background.js b/src/js/background.js index 91cf67a..98a81db 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -33,38 +33,44 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * Switch to the next or the previous tab in the current window. * - * @param {String} tabDirection - The direction in which the tab focus should change, + * @param {String} tabDirection - The direction in which the tab focus should change. + * @return {Promise} - A promise that will resolve with a response payload. * */ - gamepad.messageListenerUtils.switchTab = function (tabDirection) { - chrome.tabs.query({ currentWindow: true }, function (tabsArray) { - // Switch only if more than one tab is present. - if (tabsArray.length > 1) { - // Find index of the currently active tab. - var activeTabIndex = null; - tabsArray.forEach(function (tab, index) { - if (tab.active) { - activeTabIndex = index; - } - }); + gamepad.messageListenerUtils.switchTab = async function (tabDirection) { + var tabsArray = await chrome.tabs.query({ currentWindow: true }); + // Filter to "controllable" tabs. + var filteredTabs = tabsArray.filter(gamepad.messageListenerUtils.filterControllableTabs); - // Switch browser tab. - if (tabDirection === "previousTab") { - /** - * If the first tab is focused then switch to the last tab. - * Otherwise, switch to the previous tab. - */ - if (activeTabIndex === 0) { - activeTabIndex = tabsArray.length; - } - chrome.tabs.update(tabsArray[activeTabIndex - 1].id, { active: true }); + // Switch only if more than one tab is present. + if (filteredTabs.length > 1) { + // Find index of the currently active tab. + var activeTabIndex = null; + filteredTabs.forEach(function (tab, index) { + if (tab.active) { + activeTabIndex = index; } - else if (tabDirection === "nextTab") { - // Switch to the next tab. - chrome.tabs.update(tabsArray[(activeTabIndex + 1) % tabsArray.length].id, { active: true }); + }); + + // Switch browser tab. + if (tabDirection === "previousTab") { + /** + * If the first tab is focused then switch to the last tab. + * Otherwise, switch to the previous tab. + */ + if (activeTabIndex === 0) { + activeTabIndex = filteredTabs.length; } + await chrome.tabs.update(filteredTabs[activeTabIndex - 1].id, { active: true }); } - }); + else if (tabDirection === "nextTab") { + // Switch to the next tab. + await chrome.tabs.update(filteredTabs[(activeTabIndex + 1) % filteredTabs.length].id, { active: true }); + } + } + else { + return { vibrate: true }; + } }; /** @@ -90,12 +96,51 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE /** * * Close the currently opened window. + * @return {Promise} - A `Promise` that will complete with a response payload. * */ - gamepad.messageListenerUtils.closeCurrentWindow = function () { - chrome.windows.getCurrent(function (currentWindow) { - chrome.windows.remove(currentWindow.id); - }); + gamepad.messageListenerUtils.closeCurrentWindow = async function () { + var windowArray = await chrome.windows.getAll({ populate: true}); + var controllableWindows = windowArray.filter(gamepad.messageListenerUtils.filterControllableWindows); + if (controllableWindows.length > 1) { + var focusedWindow = false; + var focusedWindowIndex = -1; + + controllableWindows.forEach(function (window, index) { + if (window.focused) { + focusedWindow = window; + focusedWindowIndex = index; + } + }); + + if (focusedWindow) { + var newFocusIndex = (focusedWindowIndex + 1) % controllableWindows.length; + var windowToFocus = controllableWindows[newFocusIndex]; + + chrome.windows.remove(focusedWindow.id); + + chrome.windows.update(windowToFocus.id, { + focused: true + }); + } + } + else { + return { vibrate: true }; + } + }; + + // Exclude tabs whose URL begins with `chrome://`, which do not contain + // our scripts and cannot be controlled using a gamepad. + gamepad.messageListenerUtils.filterControllableTabs = function (tabElement) { + return tabElement.url && !tabElement.url.startsWith("chrome://"); + }; + + // Exclude windows that do not contain controllable tabs (see above). + gamepad.messageListenerUtils.filterControllableWindows = function (windowElement) { + if (!windowElement.tabs) { return false; } + + var filteredTabs = windowElement.tabs.filter(gamepad.messageListenerUtils.filterControllableTabs); + return filteredTabs.length > 0; }; /** @@ -105,54 +150,58 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * @param {String} windowDirection - The direction in which the window focus should change, * */ - gamepad.messageListenerUtils.switchWindow = function (windowDirection) { - chrome.windows.getLastFocused(function (focusedWindow) { - if (focusedWindow) { - chrome.windows.getAll(function (windowsArray) { - // Switch only if more than one window is present. - if (windowsArray.length > 1) { - // Find the index of the currently active window. - var focusedWindowIndex = null; - for (var index = 0; index < windowsArray.length; index++) { - if (focusedWindowIndex === null) { - var window = windowsArray[index]; - if (window.id === focusedWindow.id) { - focusedWindowIndex = index; - } - } + gamepad.messageListenerUtils.switchWindow = async function (windowDirection) { + var focusedWindow = await chrome.windows.getLastFocused(); + if (focusedWindow) { + var windowsArray = await chrome.windows.getAll({ populate: true}); + // Filter to controllable windows. + var filteredWindows = windowsArray.filter(gamepad.messageListenerUtils.filterControllableWindows); + + // Switch only if more than one window is present. + if (filteredWindows.length > 1) { + // Find the index of the currently active window. + var focusedWindowIndex = null; + for (var index = 0; index < filteredWindows.length; index++) { + if (focusedWindowIndex === null) { + var window = filteredWindows[index]; + if (window.id === focusedWindow.id) { + focusedWindowIndex = index; } + } + } - if (focusedWindowIndex === null) { - throw new Error("Can't detect focused browser window."); + if (focusedWindowIndex === null) { + throw new Error("Can't detect focused browser window."); + } + else { + var windowIndexToFocus = focusedWindowIndex; + // Switch browser window. + if (windowDirection === "previousWindow") { + if (focusedWindowIndex === 0) { + windowIndexToFocus = filteredWindows.length - 1; } else { - var windowIndexToFocus = focusedWindowIndex; - // Switch browser window. - if (windowDirection === "previousWindow") { - if (focusedWindowIndex === 0) { - windowIndexToFocus = windowsArray.length - 1; - } - else { - windowIndexToFocus = focusedWindowIndex - 1; - } - } - else if (windowDirection === "nextWindow") { - if (focusedWindowIndex >= windowsArray.length - 1) { - windowIndexToFocus = 0; - } - else { - windowIndexToFocus = focusedWindowIndex + 1; - } - } - - chrome.windows.update(windowsArray[windowIndexToFocus].id, { - focused: true - }); + windowIndexToFocus = focusedWindowIndex - 1; } } - }); + else if (windowDirection === "nextWindow") { + if (focusedWindowIndex >= filteredWindows.length - 1) { + windowIndexToFocus = 0; + } + else { + windowIndexToFocus = focusedWindowIndex + 1; + } + } + + await chrome.windows.update(filteredWindows[windowIndexToFocus].id, { + focused: true + }); + } } - }); + else { + return { vibrate: true }; + } + } }; /** @@ -160,25 +209,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Change the zoom value of the current browser tab. * * @param {String} zoomType - Determines if the page should be zoomed in or out. - * + * @return {Object} - A response payload if there are custom instructions for the client to follow up on. */ - gamepad.messageListenerUtils.setZoom = function (zoomType) { - chrome.tabs.query({ currentWindow: true, active: true }, function (currentTab) { - // Obtain the zoom value of the current tab. - chrome.tabs.getZoom(currentTab.id, function (currentZoomFactor) { - // Compute the new zoom value according to the zoom type. - var newZoomFactor = null; - if (zoomType === "zoomIn") { - newZoomFactor = currentZoomFactor + 0.1; - } - else if (zoomType === "zoomOut") { - newZoomFactor = currentZoomFactor - 0.1; - } + gamepad.messageListenerUtils.setZoom = async function (zoomType) { + var currentTab = await chrome.tabs.query({ currentWindow: true, active: true }); - // Set the new zoom value. - chrome.tabs.setZoom(currentTab.id, newZoomFactor); - }); - }); + // Obtain the zoom value of the current tab. + var currentZoomFactor = await chrome.tabs.getZoom(currentTab.id); + + // Compute the new zoom value according to the zoom type. + var newZoomFactor = null; + if (zoomType === "zoomIn") { + newZoomFactor = currentZoomFactor + 0.1; + } + else if (zoomType === "zoomOut") { + newZoomFactor = currentZoomFactor - 0.1; + } + + if (currentZoomFactor === newZoomFactor) { + return { vibrate: true }; + } + else { + // Set the new zoom value. + chrome.tabs.setZoom(currentTab.id, newZoomFactor); + } }; /** @@ -189,147 +243,203 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * @param {String} windowState - Value of the new state of the browser window. * For example, "maximized", "minimized", etc. * @param {Number} left - The position of the left edge of the screen (in pixels). + * @return {Promise} - A promise that will resolve with a response payload. * */ - gamepad.messageListenerUtils.changeWindowSize = function (that, windowState, left) { - chrome.windows.getCurrent(function (currentWindow) { - chrome.windows.update(currentWindow.id, { state: windowState }, function () { - /** - * Set the dimensions of the window if the "maximized" state doesn't work - * (Fallback Method). - */ - chrome.windows.getCurrent(function (windowPostUpdate) { - that.windowProperties[windowPostUpdate.id] = that.windowProperties[windowPostUpdate.id] || { isMaximized: null }; + gamepad.messageListenerUtils.changeWindowSize = async function (that, windowState, left) { + var currentWindow = await chrome.windows.getCurrent(); - // Value of "state" on OS X is "fullscreen" if window is maximized. - if (that.windowProperties[windowPostUpdate.id].isMaximized === null) { - that.windowProperties[windowPostUpdate.id].isMaximized = windowPostUpdate.state === "fullscreen"; - } - var isMaximized = that.windowProperties[windowPostUpdate.id].isMaximized; - - /** - * Second check is for cases when the window is not maximized during - * the first update. - */ - if (windowState === "maximized" && windowPostUpdate.state !== "maximized" && !isMaximized) { - // Preserve configuration before maximizing. - that.windowProperties[windowPostUpdate.id] = { - width: windowPostUpdate.width, - height: windowPostUpdate.height, - left: windowPostUpdate.left, - top: windowPostUpdate.top - }; - - // Update window with the new properties. - chrome.windows.update(windowPostUpdate.id, { - width: screen.width, - height: screen.height, - left: left, - top: 0 - }); - - // Update the isMaximized member variable. - that.windowProperties[windowPostUpdate.id].isMaximized = true; - } - else if (windowPostUpdate.state === "normal" && windowState === "normal" && isMaximized) { - var previousProperties = that.windowProperties[windowPostUpdate.id]; - - // Update window with the new properties. - chrome.windows.update(windowPostUpdate.id, { - width: previousProperties.width || Math.round(3 * screen.width / 5), - height: previousProperties.height || Math.round(4 * screen.height / 5), - left: previousProperties.left || (left + Math.round(screen.width / 15)), - top: previousProperties.top || Math.round(screen.height / 15) - }); - - // Update the isMaximized member variable. - that.windowProperties[windowPostUpdate.id].isMaximized = false; - } - }); + if (currentWindow.state === windowState) { + return { vibrate: true }; + } + + await chrome.windows.update(currentWindow.id, { state: windowState }); + + var windowPostUpdate = await chrome.windows.getCurrent(); + that.windowProperties[windowPostUpdate.id] = that.windowProperties[windowPostUpdate.id] || { isMaximized: null }; + + // Value of "state" on OS X is "fullscreen" if window is maximized. + if (that.windowProperties[windowPostUpdate.id].isMaximized === null) { + that.windowProperties[windowPostUpdate.id].isMaximized = windowPostUpdate.state === "fullscreen"; + } + var isMaximized = that.windowProperties[windowPostUpdate.id].isMaximized; + + /** + * Second check is for cases when the window is not maximized during + * the first update. + */ + if (windowState === "maximized" && windowPostUpdate.state !== "maximized" && !isMaximized) { + // Preserve configuration before maximizing. + that.windowProperties[windowPostUpdate.id] = { + width: windowPostUpdate.width, + height: windowPostUpdate.height, + left: windowPostUpdate.left, + top: windowPostUpdate.top + }; + + // Update window with the new properties. + await chrome.windows.update(windowPostUpdate.id, { + width: screen.width, + height: screen.height, + left: left, + top: 0 }); + + // Update the isMaximized member variable. + that.windowProperties[windowPostUpdate.id].isMaximized = true; + } + else if (windowPostUpdate.state === "normal" && windowState === "normal" && isMaximized) { + var previousProperties = that.windowProperties[windowPostUpdate.id]; + + // Update window with the new properties. + await chrome.windows.update(windowPostUpdate.id, { + width: previousProperties.width || Math.round(3 * screen.width / 5), + height: previousProperties.height || Math.round(4 * screen.height / 5), + left: previousProperties.left || (left + Math.round(screen.width / 15)), + top: previousProperties.top || Math.round(screen.height / 15) + }); + + // Update the isMaximized member variable. + that.windowProperties[windowPostUpdate.id].isMaximized = false; + } + }; + + gamepad.messageListenerUtils.search = function (actionOptions) { + chrome.search.query({ + disposition: actionOptions.disposition, + text: actionOptions.text + }); + }; + + gamepad.messageListenerUtils.openOptionsPage = async function () { + var windowsArray = await chrome.windows.getAll({ populate: true}); + var settingsWindowId = null; + var settingsTabId = null; + windowsArray.forEach(function (window) { + if (!settingsTabId) { + var settingsTab = window.tabs.find(function (tab) { + return tab.url.startsWith("chrome-extension://") && tab.url.endsWith("settings.html"); + }); + if (settingsTab) { + settingsWindowId = window.id; + settingsTabId = settingsTab.id; + } + } }); + + // If there's already a settings tab, focus on it. + if (settingsTabId) { + await chrome.windows.update(settingsWindowId, { focused: true}); + await chrome.tabs.update(settingsTabId, { active: true }); + } + // Otherwise, open a new one. + else { + return await chrome.runtime.openOptionsPage(); + } }; var messageListener = { // previous "members" windowProperties: {}, - // previous "invokers" - actionExecutor: function (actionData) { - gamepad.messageListener.actionExecutor(messageListener, actionData); + // previous "invokers", or "actions". + // On the client side, we check the control state before triggering + // these, so the method signature is simply `action(actionOptions)`. + openNewTab: async function (actionOptions) { + return await gamepad.messageListenerUtils.openNewTab(actionOptions.active, actionOptions.homepageURL); }, - // All actions are called with: tabId, invert, active, homepageURL, left - openNewTab: function (tabId, invert, active, homepageURL) { - // TODO: Currently opens a new tab and a new window. - gamepad.messageListenerUtils.openNewTab(active, homepageURL); + closeCurrentTab: async function (actionOptions) { + if (actionOptions.tabId) { + var tabs = await chrome.tabs.query({currentWindow: true }); + var controllableTabs = tabs.filter(gamepad.messageListenerUtils.filterControllableTabs); + // More than one controllable tab, just close the tab. + if (controllableTabs.length > 1) { + await chrome.tabs.remove(actionOptions.tabId); + } + // Fail over to the window close logic, which will check for controllable tabs in other windows. + else { + return await gamepad.messageListenerUtils.closeCurrentWindow(); + } + } }, - closeCurrentTab: function (tabId) { - chrome.tabs.remove(tabId); + openNewWindow: async function (actionOptions) { + return await gamepad.messageListenerUtils.openNewWindow(actionOptions.active, actionOptions.homepageURL); }, - goToPreviousTab: function () { - gamepad.messageListenerUtils.switchTab("previousTab"); + + maximizeWindow: async function (actionOptions) { + return await gamepad.messageListenerUtils.changeWindowSize(messageListener, "maximized", actionOptions.left); }, - goToNextTab: function () { - gamepad.messageListenerUtils.switchTab("nextTab"); + restoreWindowSize: async function (actionOptions) { + return await gamepad.messageListenerUtils.changeWindowSize(messageListener, "normal", actionOptions.left); }, - openNewWindow: function (tabId, invert, active, homepageURL) { - gamepad.messageListenerUtils.openNewWindow(active, homepageURL); + + // Restored windows have focus, so there is still a danger of uncontrollable windows popping up. + reopenTabOrWindow: async function () { + await chrome.sessions.restore(); }, - closeCurrentWindow: gamepad.messageListenerUtils.closeCurrentWindow, - goToPreviousWindow: function () { - gamepad.messageListenerUtils.switchWindow("previousWindow"); + goToPreviousWindow: async function () { + return await gamepad.messageListenerUtils.switchWindow("previousWindow"); }, - goToNextWindow: function () { - gamepad.messageListenerUtils.switchWindow("nextWindow"); + goToNextWindow: async function () { + return await gamepad.messageListenerUtils.switchWindow("nextWindow"); }, - zoomIn: function () { - gamepad.messageListenerUtils.setZoom("zoomIn"); + zoomIn: async function () { + return await gamepad.messageListenerUtils.setZoom("zoomIn"); }, - zoomOut: function () { - gamepad.messageListenerUtils.setZoom("zoomOut"); + zoomOut: async function () { + return await gamepad.messageListenerUtils.setZoom("zoomOut"); }, - maximizeWindow: function (tabId, invert, active, homepageURL, left) { - gamepad.messageListenerUtils.changeWindowSize(messageListener, "maximized", left); + goToPreviousTab: async function () { + return await gamepad.messageListenerUtils.switchTab("previousTab"); }, - restoreWindowSize: function (tabId, invert, active, homepageURL, left) { - gamepad.messageListenerUtils.changeWindowSize(messageListener, "normal", left); + goToNextTab: async function () { + return await gamepad.messageListenerUtils.switchTab("nextTab"); }, - reopenTabOrWindow: function () { - chrome.sessions.restore(); + closeCurrentWindow: gamepad.messageListenerUtils.closeCurrentWindow, + openActionLauncher: async function () { + return await gamepad.messageListenerUtils.openActionLauncher(); }, - openActionLauncher: function () { - gamepad.messageListenerUtils.openActionLauncher(); - } + search: gamepad.messageListenerUtils.search, + openOptionsPage: gamepad.messageListenerUtils.openOptionsPage }; - /** - * - * Calls the invoker methods according to the message is recieved from the content - * script. - * - * @param {Object} messageListener - The messageListener. - * @param {Object} actionData - The message object recieved from the content scripts. - * - */ - gamepad.messageListener.actionExecutor = function (messageListener, actionData) { - // Execute the actions only if the action data is available. - if (actionData.actionName) { - var action = messageListener[actionData.actionName]; - - // Trigger the action only if a valid action is found. - if (action) { - var invert = actionData.invert, - active = actionData.active, - left = actionData.left, - homepageURL = actionData.homepageURL; - chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + chrome.runtime.onConnect.addListener(function (port) { + port.onMessage.addListener(async function (actionOptions) { + // Execute the actions only if the action data is available. + if (actionOptions.actionName) { + var action = messageListener[actionOptions.actionName]; + + // Trigger the action only if a valid action is found. + if (action) { + var tabs = await chrome.tabs.query({ active: true, currentWindow: true }); var tabId = tabs[0] ? tabs[0].id : undefined; - action(tabId, invert, active, homepageURL, left); - }); + + var wrappedActionOptions = JSON.parse(JSON.stringify(actionOptions)); + wrappedActionOptions.tabId = tabId; + + var actionResult = await action(wrappedActionOptions); + port.postMessage(actionResult); + port.disconnect(); + } } - } - }; + }); + }); + + // If the user's preferences allow us to, open the settings panel on startup. + chrome.runtime.onStartup.addListener(function () { + chrome.storage.local.get(["gamepad-prefs"], function (storedObject) { + var storedPrefs = storedObject && storedObject["gamepad-prefs"]; + // If our prefs exactly match the defaults, nothing is stored. + // In this case, the default behaviour is to open a window, so if + // there are no stored settings, we do that. + if (!storedPrefs || storedPrefs["openWindowOnStartup"]) { + chrome.runtime.openOptionsPage(); + } + }); + }); - // Instantiate and add action handler as a listener. - chrome.runtime.onMessage.addListener(messageListener.actionExecutor); + // Open the settings panel when the icon is clicked. + chrome.action.onClicked.addListener(() => { + chrome.runtime.openOptionsPage(); + }); })(); diff --git a/src/js/configuration_panel/button-listeners.js b/src/js/configuration_panel/button-listeners.js index a44ed4c..1df3c7d 100644 --- a/src/js/configuration_panel/button-listeners.js +++ b/src/js/configuration_panel/button-listeners.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/src/js/configuration_panel/configuration-panel.js b/src/js/configuration_panel/configuration-panel.js index 8008e0d..675b230 100644 --- a/src/js/configuration_panel/configuration-panel.js +++ b/src/js/configuration_panel/configuration-panel.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -99,7 +99,8 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE sendArrowRight: "Send right arrow to the focused element.", sendArrowUp: "Send up arrow to the focused element.", sendArrowDown: "Send down arrow to the focused element.", - openActionLauncher: "Open Action Launcher" + openActionLauncher: "Open Action Launcher", + openSearchKeyboard: "Open Search" }, axes: { null: "None", diff --git a/src/js/configuration_panel/create-panel-utils.js b/src/js/configuration_panel/create-panel-utils.js index 2ca34af..181c264 100644 --- a/src/js/configuration_panel/create-panel-utils.js +++ b/src/js/configuration_panel/create-panel-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/src/js/configuration_panel/panel-events.js b/src/js/configuration_panel/panel-events.js index e475ccf..121b97a 100644 --- a/src/js/configuration_panel/panel-events.js +++ b/src/js/configuration_panel/panel-events.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/src/js/content_scripts/action-launcher.js b/src/js/content_scripts/action-launcher.js index 438d8a9..ae02879 100644 --- a/src/js/content_scripts/action-launcher.js +++ b/src/js/content_scripts/action-launcher.js @@ -18,6 +18,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE fluid.defaults("gamepad.actionLauncher", { gradeNames: ["gamepad.modal"], model: { + classNames: " actionLauncher-modal", label: "Gamepad Navigator: Launch Action" }, components: { @@ -36,13 +37,18 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, model: { + row: -1, value: 1, oldValue: 0, - speedFactor: 1, - invert: false, - background: false, - homepageURL: "https://www.google.com/", - frequency: 100 + commonConfiguration: { + homepageURL: "https://www.google.com/" + }, + actionOptions: { + speedFactor: 1, + invert: false, + background: false, + frequency: 100 + } }, markup: { @@ -57,6 +63,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE handleKeydown: { funcName: "gamepad.actionLauncher.action.handleKeydown", args: ["{that}", "{arguments}.0"] // event + }, + handleFocus: { + funcName: "gamepad.actionLauncher.action.handleFocus", + args: ["{that}", "{arguments}.0"] // event } }, @@ -70,10 +80,32 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE this: "{that}.container", method: "keydown", args: ["{that}.handleKeydown"] + }, + "onCreate.bindFocus": { + this: "{that}.container", + method: "focus", + args: ["{that}.handleFocus"] + } + }, + + modelListeners: { + "focusedRow": { + funcName: "gamepad.actionLauncher.action.handleFocusedRowChange", + args: ["{that}", "{that}.model.focusedRow", "{that}.model.row"] } } }); + gamepad.actionLauncher.action.handleFocusedRowChange = function (that, focusedRow, row) { + if (row === focusedRow) { + that.container[0].focus(); + } + }; + + gamepad.actionLauncher.action.handleFocus = function (that) { + that.applier.change("focusedRow", that.model.row); + }; + gamepad.actionLauncher.action.handleClick = function (actionComponent, inputMapperComponent, modalComponent, event) { event.preventDefault(); @@ -86,27 +118,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // triggered appropriately. // All actions are called with: - // value, speedFactor, invert, background, oldValue, homepageURL + // value, oldValue, actionOptions // Simulate button down actionFn( 1, - actionComponent.model.speedFactor, - actionComponent.model.invert, - actionComponent.model.background, 0, - actionComponent.model.homepageURL + actionComponent.model.actionOptions ); // Simulate button up after a delay (100ms by default) setTimeout(function () { actionFn( 0, - actionComponent.model.speedFactor, - actionComponent.model.invert, - actionComponent.model.background, 1, - actionComponent.model.homepageURL + actionComponent.model.actionOptions ); }, actionComponent.model.frequency); } @@ -126,93 +152,113 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, model: { + focusedRow: 0, // TODO: Add support for controlling `backgroundOption` in the `openNewTab` and `openNewWindow` actions. - actionDefs: { - // All button-driven actions, except for the action launcher itself. - click: { - description: "Click" - }, - previousPageInHistory: { - description: "History back button" - }, - nextPageInHistory: { - description: "History next button" - }, - reverseTab: { - description: "Focus on the previous element" + actionDefs: [ + // All button-driven actions, except for the action launcher itself, ordered by subjective "usefulness". + { + key: "openConfigPanel", + description: "Configure Gamepad Navigator" }, - forwardTab: { - description: "Focus on the next element" + { + key: "openSearchKeyboard", + description: "Search" }, - scrollLeft: { - description: "Scroll left", - frequency: 250 + { + key: "openNewWindow", + description: "Open a new browser window" }, - scrollRight: { - description: "Scroll right", - frequency: 250 + { + key: "openNewTab", + description: "Open a new tab" }, - scrollUp: { - description: "Scroll up", - frequency: 250 + { + key: "goToPreviousWindow", + description: "Switch to the previous browser window" }, - scrollDown: { - description: "Scroll down", - frequency: 250 + { + key: "goToNextWindow", + description: "Switch to the next browser window" }, - goToPreviousTab: { + { + key: "goToPreviousTab", description: "Switch to the previous browser tab" }, - goToNextTab: { + { + key: "goToNextTab", description: "Switch to the next browser tab" }, - closeCurrentTab: { + { + key: "closeCurrentTab", description: "Close current browser tab" }, - openNewTab: { - description: "Open a new tab" - }, - closeCurrentWindow: { + { + key: "closeCurrentWindow", description: "Close current browser window" }, - openNewWindow: { - description: "Open a new browser window" - }, - goToPreviousWindow: { - description: "Switch to the previous browser window" - }, - goToNextWindow: { - description: "Switch to the next browser window" + { + key: "reopenTabOrWindow", + description: "Re-open the last closed tab or window" }, - zoomIn: { - description: "Zoom-in on the active web page" + { + key: "previousPageInHistory", + description: "History back button" }, - zoomOut: { - description: "Zoom-out on the active web page" + { + key: "nextPageInHistory", + description: "History next button" }, - maximizeWindow: { + { + key: "maximizeWindow", description: "Maximize the current browser window" }, - restoreWindowSize: { + { + key: "restoreWindowSize", description: "Restore the size of current browser window" }, - reopenTabOrWindow: { - description: "Re-open the last closed tab or window" + // These should nearly always already be bound. + { + key: "click", + description: "Click" }, - sendArrowLeft: { - description: "Send left arrow to the focused element." + { + key: "reverseTab", + description: "Focus on the previous element" + }, + { + key: "forwardTab", + description: "Focus on the next element" }, - sendArrowRight: { - description: "Send right arrow to the focused element." + // Here for completeness, but IMO less likely to be used. + { + key: "scrollLeft", + description: "Scroll left", + frequency: 250 }, - sendArrowUp: { - description: "Send up arrow to the focused element." + { + key: "scrollRight", + description: "Scroll right", + frequency: 250 + }, + { + key: "scrollUp", + description: "Scroll up", + frequency: 250 + }, + { + key: "scrollDown", + description: "Scroll down", + frequency: 250 + }, + { + key: "zoomIn", + description: "Zoom-in on the active web page" }, - sendArrowDown: { - description: "Send down arrow to the focused element." + { + key: "zoomOut", + description: "Zoom-out on the active web page" } - // TODO: Add action to open configuration menu, when available. - } + ] }, dynamicComponents: { @@ -222,15 +268,53 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE sources: "{that}.model.actionDefs", options: { model: { - actionKey: "{sourcePath}", + focusedRow: "{gamepad.actionLauncher.actionsPanel}.model.focusedRow", + row: "{sourcePath}", + actionKey: "{source}.key", description: "{source}.description", - speedFactor: "{source}.speedFactor", - invert: "{source}.invert", - background: "{source}.background", - frequency: "{source}.frequency" + actionOptions: { + speedFactor: "{source}.speedFactor", + invert: "{source}.invert", + background: "{source}.background", + frequency: "{source}.frequency" + } } } } + }, + + invokers: { + handleKeydown: { + funcName: "gamepad.actionLauncher.actionsPanel.handleKeydown", + args: ["{that}", "{arguments}.0"] // event + } + }, + + listeners: { + "onCreate.bindKeydown": { + this: "{that}.container", + method: "keydown", + args: ["{that}.handleKeydown"] + } } }); + + gamepad.actionLauncher.actionsPanel.handleKeydown = function (that, event) { + if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.code)) { + event.preventDefault(); + var lastRow = that.model.actionDefs.length - 1; + switch (event.code) { + case "ArrowLeft": + case "ArrowUp": + var previousRow = that.model.focusedRow > 0 ? that.model.focusedRow - 1 : lastRow; + that.applier.change("focusedRow", previousRow); + break; + case "ArrowRight": + case "ArrowDown": + var nextRow = that.model.focusedRow < lastRow ? that.model.focusedRow + 1 : 0; + that.applier.change("focusedRow", nextRow); + break; + } + } + }; })(fluid); diff --git a/src/js/content_scripts/bindings.js b/src/js/content_scripts/bindings.js new file mode 100644 index 0000000..25827f8 --- /dev/null +++ b/src/js/content_scripts/bindings.js @@ -0,0 +1,132 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + fluid.registerNamespace("gamepad.bindings"); + + /* + + "Action options", which represent the action to be performed in bindings and in internal calls between functions. + + @typedef {Object} ActionOptions + @property {String} action - The name of the action. + @property {Boolean} [invert] - Whether to invert the direction of motion (for actions that have a direction, like scrolling). + @property {Number} [repeat] - For actions that support continuous operation, how many seconds to wait before repeating the action if the same control is still depressed. + @property {Number} [speed] - How far to navigate in a single action. + @property {Boolean} [background] - For new windows/tabs, whether to open in the background. + @property {String} [key] - For `sendKey`, the key to send. + + */ + + gamepad.bindings.defaults = { + buttons: { + // Cross on PS controller, A on Xbox + 0: { + action: "click" + }, + + // Circle on PS controller, B on Xbox. + // TODO: Make this work only in modals. + 1: { + action: "sendKey", + key: "Escape" + }, + + // Left Bumper. + "4": { + action: "reverseTab", + repeat: 1, + speed: 2.5 + }, + // Right Bumper. + "5": { + action: "forwardTab", + repeat: 1, + speed: 2.5 + }, + + // Select button. + 8: { + action: "openSearchKeyboard" + }, + + // Start button. + 9: { + action: "openActionLauncher" + }, + + // D-pad. + // Up. + 12: { + action: "sendKey", + key: "ArrowUp", + repeat: 1, + speed: 1 + }, + // Down + 13: { + action: "sendKey", + key: "ArrowDown", + repeat: 1, + speed: 1 + }, + // Left + 14: { + action: "sendKey", + key: "ArrowLeft", + repeat: 1, + speedFactor: 1 + }, + // Right. + 15: { + action: "sendKey", + key: "ArrowRight", + repeat: 1, + speed: 1 + }, + + // "Badge" button. + 16: { + action: "openConfigPanel" + } + }, + axes: { + // Left thumbstick horizontal axis. + "0": { + action: "scrollHorizontally", + repeat: 1, + speed: 1, + invert: false + }, + // Left thumbstick vertical axis. + "1": { + action: "scrollVertically", + repeat: 1, + speed: 1, + invert: false + } + } + }; +})(fluid); diff --git a/src/js/content_scripts/gamepad-navigator.js b/src/js/content_scripts/gamepad-navigator.js index 4059b71..8c02565 100644 --- a/src/js/content_scripts/gamepad-navigator.js +++ b/src/js/content_scripts/gamepad-navigator.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -19,6 +19,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE fluid.defaults("gamepad.navigator", { gradeNames: ["fluid.modelComponent"], model: { + // TODO: Figure out how this is used and how it differs from "in view"; connected: false, axes: {}, buttons: {} diff --git a/src/js/content_scripts/input-mapper-background-utils.js b/src/js/content_scripts/input-mapper-background-utils.js index 13c5a7d..21d8d88 100644 --- a/src/js/content_scripts/input-mapper-background-utils.js +++ b/src/js/content_scripts/input-mapper-background-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -25,39 +25,40 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * when the user navigates back to the same tab. */ + gamepad.inputMapperUtils.background.postMessageOnControlDown = function (that, value, oldValue, actionOptions) { + if (oldValue === 0 && value > that.model.prefs.analogCutoff) { + gamepad.inputMapperUtils.background.postMessage(that, actionOptions); + } + }; + /** * - * Sends message to the background script to perform the given action. + * Connect to the background script, send a message, and handle the response. * * @param {Object} that - The inputMapper component. - * @param {String} actionName - The action to be performed. - * @param {Integer} value - The value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Boolean} background - Whether the new tab should open in background. - * @param {String} homepageURL - The URL for the new tab. + * @param {String} actionOptions - The action payload to be transmitted. * */ - gamepad.inputMapperUtils.background.sendMessage = function (that, actionName, value, oldValue, background, homepageURL) { - if (value < oldValue && oldValue > that.options.cutoffValue) { - // Set the data object for the action. - var actionData = { actionName: actionName }; - - // Set active key if background parameter is passed as an argument. - if (background !== undefined) { - actionData.active = !background; - } - if (homepageURL) { - actionData.homepageURL = homepageURL; + gamepad.inputMapperUtils.background.postMessage = async function (that, actionOptions) { + // We use this because chrome.runtime.sendMessage did not leave enough time to receive a response. + var port = chrome.runtime.connect(); + port.onMessage.addListener(function (response) { + var vibrate = fluid.get(response, "vibrate"); + if (vibrate) { + that.vibrate(); } + }); - // Set the left pixel if the action is about changing "window size". - if (actionName === "maximizeWindow" || actionName === "restoreWindowSize") { - actionData.left = screen.availLeft; - } + var wrappedActionOptions = fluid.copy(actionOptions); + + wrappedActionOptions.homepageURL = that.model.commonConfiguration.homepageURL; - // Send the message to the background script with the action details. - chrome.runtime.sendMessage(actionData); + // Set the left pixel if the action is about changing "window size". + if (actionOptions.actionName === "maximizeWindow" || actionOptions.actionName === "restoreWindowSize") { + wrappedActionOptions.left = screen.availLeft; } + + port.postMessage(wrappedActionOptions); }; /** @@ -70,19 +71,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * @param {Boolean} invert - Whether the zooming should be in opposite order. * */ - gamepad.inputMapperUtils.background.thumbstickZoom = function (that, value, invert) { + gamepad.inputMapperUtils.background.thumbstickZoom = async function (that, value, invert) { + clearInterval(that.intervalRecords.zoomIn); + clearInterval(that.intervalRecords.zoomOut); + // Get the updated input value according to the configuration. var inversionFactor = invert ? -1 : 1; - value = value * inversionFactor; - var zoomType = value > 0 ? "zoomOut" : "zoomIn", - actionData = { actionName: zoomType }; - value = value * (value > 0 ? 1 : -1); + var polarisedValue = value * inversionFactor; + var zoomType = polarisedValue > 0 ? "zoomOut" : "zoomIn"; + var actionOptions = { actionName: zoomType }; // Call the zoom changing invokers according to the input values. - clearInterval(that.intervalRecords.zoomIn); - clearInterval(that.intervalRecords.zoomOut); - if (value > that.options.cutoffValue) { - that.intervalRecords[zoomType] = setInterval(chrome.runtime.sendMessage, that.options.frequency, actionData); + if (Math.abs(value) > that.options.cutoffValue) { + that.intervalRecords[zoomType] = setInterval(function (actionOptions) { + gamepad.inputMapperUtils.background.postMessage(that, actionOptions); + }, that.options.frequency, actionOptions); } }; @@ -98,23 +101,19 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE */ gamepad.inputMapperUtils.background.thumbstickWindowSize = function (that, value, invert) { // Get the updated input value according to the configuration. - var inversionFactor = invert ? -1 : 1, - windowSizeActionLabel = null; - value = value * inversionFactor; - if (value > 0) { - windowSizeActionLabel = "maximizeWindow"; - } - else { - windowSizeActionLabel = "restoreWindowSize"; - value = value * -1; - } + var inversionFactor = invert ? -1 : 1; + var polarisedValue = value * inversionFactor; + + var actionName = polarisedValue > 0 ? "maximizeWindow" : "restoreWindowSize"; + + var actionOptions = { + actionName: actionName, + left: screen.availLeft + }; // Call the window size changing invokers according to the input value. - if (value > that.options.cutoffValue) { - chrome.runtime.sendMessage({ - actionName: windowSizeActionLabel, - left: screen.availLeft - }); + if (Math.abs(value) > that.options.cutoffValue) { + gamepad.inputMapperUtils.background.postMessage(that, actionOptions); } }; })(fluid); diff --git a/src/js/content_scripts/input-mapper-base.js b/src/js/content_scripts/input-mapper-base.js index 39bd52d..26f09ce 100644 --- a/src/js/content_scripts/input-mapper-base.js +++ b/src/js/content_scripts/input-mapper-base.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -16,12 +16,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE "use strict"; var gamepad = fluid.registerNamespace("gamepad"); - fluid.registerNamespace("gamepad.configMaps"); - fluid.registerNamespace("gamepad.inputMapper.base"); - fluid.registerNamespace("gamepad.inputMapperUtils.content"); + // TODO: Fairly sure none of these are required. + // fluid.registerNamespace("gamepad.configMaps"); + // fluid.registerNamespace("gamepad.inputMapper.base"); + // fluid.registerNamespace("gamepad.inputMapperUtils.content"); fluid.defaults("gamepad.inputMapper.base", { gradeNames: ["gamepad.configMaps", "gamepad.navigator"], + model: { + pageInView: true + }, modelListeners: { "axes.*": { funcName: "{that}.produceNavigation", @@ -47,6 +51,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * TODO: Move the member variables used for the inter-navigation web page * features to the "inputMapper" component. */ + // TODO: These should be expressed per control rather than per action, as we might bind an action to more than one control. intervalRecords: { upwardScroll: null, downwardScroll: null, @@ -63,13 +68,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, currentTabIndex: 0, tabbableElements: null, - mutationObserverInstance: null + mutationObserverInstance: null, + // TODO: Ensure that there are sensible defaults somewhere. + prefs: {}, + bindings: {} }, // TODO: Make this configurable. // "Jitter" cutoff Value for analog thumb sticks. - cutoffValue: 0.40, - scrollInputMultiplier: 50, + cutoffValue: 0.40, // TODO: Make this a preference + scrollInputMultiplier: 50, // TODO: Make this a preference + invokers: { + updateTabbables: { + funcName: "gamepad.inputMapper.base.updateTabbables", + args: ["{that}"] + }, produceNavigation: { funcName: "gamepad.inputMapper.base.produceNavigation", args: ["{that}", "{arguments}.0"] @@ -86,19 +99,15 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE "this": "{that}.mutationObserverInstance", method: "disconnect" }, - // TODO: Investigate, identify, and fix tab navigation issues. - tabindexSortFilter: { - funcName: "gamepad.inputMapper.base.tabindexSortFilter", - args: ["{arguments}.0", "{arguments}.1"] + vibrate: { + funcName: "gamepad.inputMapper.base.vibrate", + args: ["{that}"] }, - /** - * TODO: Add tests for links and other elements that involve navigation - * between pages. - */ - // Actions, these are called with: value, speedFactor, invert, background, oldValue, homepageURL + + // Actions are called with value, oldValue, actionOptions click: { funcName: "gamepad.inputMapperUtils.content.click", - args: ["{arguments}.0"] + args: ["{that}", "{arguments}.0"] }, previousPageInHistory: { funcName: "gamepad.inputMapperUtils.content.previousPageInHistory", @@ -108,6 +117,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE funcName: "gamepad.inputMapperUtils.content.nextPageInHistory", args: ["{that}", "{arguments}.0"] }, + reverseTab: { funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation", args: ["{that}", "{arguments}.0", "reverseTab"] @@ -116,21 +126,22 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation", args: ["{that}", "{arguments}.0", "forwardTab"] }, + scrollLeft: { funcName: "gamepad.inputMapperUtils.content.scrollLeft", - args: ["{that}", "{arguments}.0", "{arguments}.1"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, scrollRight: { funcName: "gamepad.inputMapperUtils.content.scrollRight", - args: ["{that}", "{arguments}.0", "{arguments}.1"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, scrollUp: { funcName: "gamepad.inputMapperUtils.content.scrollUp", - args: ["{that}", "{arguments}.0", "{arguments}.1"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, scrollDown: { funcName: "gamepad.inputMapperUtils.content.scrollDown", - args: ["{that}", "{arguments}.0", "{arguments}.1"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, scrollHorizontally: { funcName: "gamepad.inputMapperUtils.content.scrollHorizontally", @@ -147,33 +158,40 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // TODO: Add tests for when the number of tabbable elements changes. thumbstickTabbing: { funcName: "gamepad.inputMapperUtils.content.thumbstickTabbing", - args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + args: ["{that}", "{arguments}.0", "{arguments}.2"] + }, + + sendKey: { + funcName: "gamepad.inputMapperUtils.content.sendKey", + args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, actionOptions }, + + // TODO: Remove these once we are using the new bindings instead of the old "map". // Arrow actions for buttons sendArrowLeft: { funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{arguments}.0", "ArrowLeft"] // value, key + args: ["{that}", "{arguments}.0", { key: "ArrowLeft" }] // value, actionOptions }, sendArrowRight: { funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{arguments}.0", "ArrowRight"] // value, key + args: ["{that}", "{arguments}.0", { key: "ArrowRight" }] // value, actionOptions }, sendArrowUp: { funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{arguments}.0", "ArrowUp"] // value, key + args: ["{that}", "{arguments}.0", { key: "ArrowUp" }] // value, actionOptions }, sendArrowDown: { funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{arguments}.0", "ArrowDown"] // value, key + args: ["{that}", "{arguments}.0", { key: "ArrowDown" }] // value, actionOptions }, // Arrow actions for axes thumbstickHorizontalArrows: { funcName: "gamepad.inputMapperUtils.content.thumbstickArrows", - args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowRight", "ArrowLeft"] // value, speedFactor, invert, forwardKey, backwardKey + args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowRight", "ArrowLeft"] // value, actionOptions, forwardKey, backwardKey }, thumbstickVerticalArrows: { funcName: "gamepad.inputMapperUtils.content.thumbstickArrows", - args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowDown", "ArrowUp"] // value, speedFactor, invert, forwardKey, backwardKey + args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowDown", "ArrowUp"] // value, actionOptions, forwardKey, backwardKey } } }); @@ -189,46 +207,50 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * configured action map to produce a navigation effect. * * @param {Object} that - The inputMapper component. - * @param {Object} change - The recipt for the change in input values. + * @param {Object} change - The receipt for the change in input values. * */ gamepad.inputMapper.base.produceNavigation = function (that, change) { - /** - * Check if input is generated by axis or button and which button/axes was - * disturbed. - */ - var inputType = change.path[0], - index = change.path[1], - inputValue = change.value, - oldInputValue = change.oldValue || 0; + // Only respond to gamepad input if we are "in view". + if (that.model.pageInView) { + /** + * Check if input is generated by axis or button and which button/axes was + * disturbed. + */ + var inputType = change.path[0], // i. e. "button", or "axis" + index = change.path[1], // i.e. 0, 1, 2 + inputValue = change.value, + oldInputValue = change.oldValue || 0; - var inputProperties = that.model.map[inputType][index], - actionLabel = fluid.get(inputProperties, "currentAction") || fluid.get(inputProperties, "defaultAction"), - homepageURL = that.model.commonConfiguration.homepageURL; + // Look for a binding at map.axis.0, map.button.1, et cetera. + var binding = that.model.map[inputType][index]; + // TODO: See how/whether we ever fail over using this structure. + var actionLabel = fluid.get(binding, "currentAction") || fluid.get(binding, "defaultAction"); + + /** + * TODO: Modify the action call in such a manner that the action gets triggered + * when the inputs are released. + * (To gain shortpress and longpress actions) + * + * Refer: + * https://github.com/fluid-lab/gamepad-navigator/pull/21#discussion_r453507050 + */ - /** - * TODO: Modify the action call in such a manner that the action gets triggered - * when the inputs are released. - * (To gain shortpress and longpress actions) - * - * Refer: - * https://github.com/fluid-lab/gamepad-navigator/pull/21#discussion_r453507050 - */ + // Execute the actions only if the action label is available. + if (actionLabel) { + var action = fluid.get(that, actionLabel); - // Execute the actions only if the action label is available. - if (actionLabel) { - var action = fluid.get(that, actionLabel); + // Trigger the action only if a valid function is found. + if (action) { + var actionOptions = fluid.copy(binding); + actionOptions.homepageURL = that.model.commonConfiguration.homepageURL; - // Trigger the action only if a valid function is found. - if (action) { - action( - inputValue, - inputProperties.speedFactor, - inputProperties.invert, - inputProperties.background, - oldInputValue, - homepageURL - ); + action( + inputValue, + oldInputValue, + actionOptions + ); + } } } }; @@ -247,6 +269,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }); }; + gamepad.inputMapper.base.updateTabbables = function (that) { + that.tabbableElements = ally.query.tabsequence({ strategy: "strict" }); + }; + + /** * * A listener to track DOM elements and update the list when the DOM is updated. @@ -259,13 +286,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE MutationObserver = that.options.windowObject.MutationObserver; // Record the tabbable elements when the component is created. - that.tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter); + that.updateTabbables(); // Create an instance of the mutation observer. - that.mutationObserverInstance = new MutationObserver(function () { - // Record the tabbable elements when the DOM mutates. - that.tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter); - }); + that.mutationObserverInstance = new MutationObserver(that.updateTabbables); // Specify the mutations to be observed. var observerConfiguration = { @@ -281,37 +305,25 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE that.mutationObserverInstance.observe(body, observerConfiguration); }; - /** - * - * Filter for sorting the elements; to be used inside JavaScript's sort() method. - * - * @param {Object} elementOne - The DOM element. - * @param {Object} elementTwo - The DOM element. - * @return {Integer} - The value which will decide the order of the two elements. - * - */ - gamepad.inputMapper.base.tabindexSortFilter = function (elementOne, elementTwo) { - var tabindexOne = parseInt(elementOne.getAttribute("tabindex")), - tabindexTwo = parseInt(elementTwo.getAttribute("tabindex")); - /** - * If both elements have tabindex greater than 0, arrange them in ascending order - * of the tabindex. Otherwise if only one of the elements have tabindex greater - * than 0, place it before the other element. And in case, no element has a - * tabindex attribute or both of them posses tabindex value equal to 0, keep them - * in the same order. - */ - if (tabindexOne > 0 && tabindexTwo > 0) { - return tabindexOne - tabindexTwo; - } - else if (tabindexOne > 0) { - return -1; - } - else if (tabindexTwo > 0) { - return 1; - } - else { - return 0; + gamepad.inputMapper.base.vibrate = function (that) { + if (that.model.prefs.vibrate) { + var gamepads = that.options.windowObject.navigator.getGamepads(); + var nonNullGamepads = gamepads.filter(function (gamepad) { return gamepad !== null; }); + + fluid.each(nonNullGamepads, function (gamepad) { + if (gamepad.vibrationActuator) { + gamepad.vibrationActuator.playEffect( + "dual-rumble", + { + duration: 250, + startDelay: 0, + strongMagnitude: 0.25, + weakMagnitude: .75 + } + ); + } + }); } }; })(fluid); diff --git a/src/js/content_scripts/input-mapper-content-utils.js b/src/js/content_scripts/input-mapper-content-utils.js index 94bc08e..6c8a05d 100644 --- a/src/js/content_scripts/input-mapper-content-utils.js +++ b/src/js/content_scripts/input-mapper-content-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -12,13 +12,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE /* global gamepad, ally, chrome */ -(function (fluid, $) { +(function (fluid) { "use strict"; fluid.registerNamespace("gamepad.inputMapperUtils.content"); - // TODO: Fix argument type in JSDoc comments, especially "value" and "speedFactor". - /** * TODO: Fix the "speedFactor" usage in invokers to reduce the given interval loop * frequency. @@ -29,26 +27,28 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll horizontally across the webpage. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. - * @param {Boolean} invert - Whether the scroll should be in opposite order. + * @param {Integer} value - The current value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor, invert). * */ - gamepad.inputMapperUtils.content.scrollHorizontally = function (that, value, speedFactor, invert) { - // Get the updated input value according to the configuration. - var inversionFactor = invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - clearInterval(that.intervalRecords.leftScroll); - that.scrollRight(value, speedFactor); - } - else if (value < 0) { - clearInterval(that.intervalRecords.rightScroll); - that.scrollLeft(-1 * value, speedFactor); - } - else { - clearInterval(that.intervalRecords.leftScroll); - clearInterval(that.intervalRecords.rightScroll); + gamepad.inputMapperUtils.content.scrollHorizontally = function (that, value, oldValue, actionOptions) { + if (that.model.pageInView) { + // Get the updated input value according to the configuration. + var inversionFactor = actionOptions.invert ? -1 : 1; + value = value * inversionFactor; + if (value > 0) { + clearInterval(that.intervalRecords.leftScroll); + that.scrollRight(value, oldValue, actionOptions); + } + else if (value < 0) { + clearInterval(that.intervalRecords.rightScroll); + that.scrollLeft(-1 * value, oldValue, actionOptions); + } + else { + clearInterval(that.intervalRecords.leftScroll); + clearInterval(that.intervalRecords.rightScroll); + } } }; @@ -57,11 +57,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in left direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. + * @param {Integer} value - The current value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.scrollLeft = function (that, value, speedFactor) { + gamepad.inputMapperUtils.content.scrollLeft = function (that, value, oldValue, actionOptions) { + var speedFactor = actionOptions.speedFactor || 1; + /** * Stop scrolling for the previous input value. Also stop scrolling if the input * source (analog/button) is at rest. @@ -72,11 +75,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage towards the left only if the input value is more than the * cutoff value. */ - if (value > that.options.cutoffValue) { + if (that.model.pageInView && (value > that.options.cutoffValue)) { // Scroll to the left according to the new input value. that.intervalRecords.leftScroll = setInterval(function () { - var xOffset = $(that.options.windowObject).scrollLeft(); - $(that.options.windowObject).scrollLeft(xOffset - value * that.options.scrollInputMultiplier * speedFactor); + if (window.scrollX > 0) { + window.scroll(window.scrollX - value * that.options.scrollInputMultiplier * speedFactor, window.scrollY); + } + else { + clearInterval(that.intervalRecords.leftScroll); + that.vibrate(); + } + }, that.options.frequency); } }; @@ -86,11 +95,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage towards the right direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. + * @param {Integer} value - The current value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.scrollRight = function (that, value, speedFactor) { + gamepad.inputMapperUtils.content.scrollRight = function (that, value, oldValue, actionOptions) { + var speedFactor = actionOptions.speedFactor || 1; + /** * Stop scrolling for the previous input value. Also stop scrolling if the input * source (analog/button) is at rest. @@ -101,11 +113,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage towards the right only if the input value is more than the * cutoff value. */ - if (value > that.options.cutoffValue) { + if (that.model.pageInView && (value > that.options.cutoffValue)) { // Scroll to the right according to the new input value. that.intervalRecords.rightScroll = setInterval(function () { - var xOffset = $(that.options.windowObject).scrollLeft(); - $(that.options.windowObject).scrollLeft(xOffset + value * that.options.scrollInputMultiplier * speedFactor); + window.scroll(window.scrollX + value * that.options.scrollInputMultiplier * speedFactor, window.scrollY); + + var documentWidth = document.body.scrollWidth; + var currentScrollX = window.scrollX + window.innerWidth; + if (currentScrollX >= documentWidth) { + clearInterval(that.intervalRecords.rightScroll); + that.vibrate(); + } }, that.options.frequency); } }; @@ -115,26 +133,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll vertically across the webpage. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. - * @param {Boolean} invert - Whether the scroll should be in opposite order. + * @param {Integer} value - The currrent value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.scrollVertically = function (that, value, speedFactor, invert) { - // Get the updated input value according to the configuration. - var inversionFactor = invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - clearInterval(that.intervalRecords.upwardScroll); - that.scrollDown(value, speedFactor); - } - else if (value < 0) { - clearInterval(that.intervalRecords.downwardScroll); - that.scrollUp(-1 * value, speedFactor); - } - else { - clearInterval(that.intervalRecords.upwardScroll); - clearInterval(that.intervalRecords.downwardScroll); + gamepad.inputMapperUtils.content.scrollVertically = function (that, value, oldValue, actionOptions) { + var speedFactor = actionOptions.speedFactor || 1; + + if (that.model.pageInView) { + // Get the updated input value according to the configuration. + var inversionFactor = actionOptions.invert ? -1 : 1; + value = value * inversionFactor; + if (value > 0) { + clearInterval(that.intervalRecords.upwardScroll); + that.scrollDown(value, oldValue, speedFactor); + } + else if (value < 0) { + clearInterval(that.intervalRecords.downwardScroll); + that.scrollUp(-1 * value, oldValue, speedFactor); + } + else { + clearInterval(that.intervalRecords.upwardScroll); + clearInterval(that.intervalRecords.downwardScroll); + } } }; @@ -143,11 +165,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in upward direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. + * @param {Integer} value - The current value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.scrollUp = function (that, value, speedFactor) { + gamepad.inputMapperUtils.content.scrollUp = function (that, value, oldValue, actionOptions) { + var speedFactor = actionOptions.speedFactor || 1; + /** * Stop scrolling for the previous input value. Also stop scrolling if the input * source (analog/button) is at rest. @@ -158,11 +183,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage upward only if the input value is more than the cutoff * value. */ - if (value > that.options.cutoffValue) { + if (that.model.pageInView && (value > that.options.cutoffValue)) { // Scroll upward according to the new input value. that.intervalRecords.upwardScroll = setInterval(function () { - var yOffset = $(that.options.windowObject).scrollTop(); - $(that.options.windowObject).scrollTop(yOffset - value * that.options.scrollInputMultiplier * speedFactor); + if (window.scrollY > 0) { + window.scroll(window.scrollX, window.scrollY - value * that.options.scrollInputMultiplier * speedFactor); + } + else { + clearInterval(that.intervalRecords.upwardScroll); + that.vibrate(); + } }, that.options.frequency); } }; @@ -172,11 +202,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in downward direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input (in pixels). - * @param {Integer} speedFactor - Times by which the scroll speed should be increased. + * @param {Integer} value - The current value of the gamepad input. + * @param {Integer} oldValue - The previous value of the gamepad input. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.scrollDown = function (that, value, speedFactor) { + gamepad.inputMapperUtils.content.scrollDown = function (that, value, oldValue, actionOptions) { + var speedFactor = actionOptions.speedFactor || 1; + /** * Stop scrolling for the previous input value. Also stop scrolling if the input * source (analog/button) is at rest. @@ -187,11 +220,19 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage downward only if the input value is more than the cutoff * value. */ - if (value > that.options.cutoffValue) { + if (that.model.pageInView && (value > that.options.cutoffValue)) { // Scroll upward according to the new input value. that.intervalRecords.downwardScroll = setInterval(function () { - var yOffset = $(that.options.windowObject).scrollTop(); - $(that.options.windowObject).scrollTop(yOffset + value * that.options.scrollInputMultiplier * speedFactor); + window.scroll(window.scrollX, window.scrollY + value * that.options.scrollInputMultiplier * speedFactor); + + // Adapted from: + // https://fjolt.com/article/javascript-check-if-user-scrolled-to-bottom + var documentHeight = document.body.scrollHeight; + var currentScroll = window.scrollY + window.innerHeight; + if (currentScroll >= documentHeight) { + clearInterval(that.intervalRecords.downwardScroll); + that.vibrate(); + }; }, that.options.frequency); } }; @@ -202,28 +243,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * @param {Object} that - The inputMapper component. * @param {Integer} value - The value of the gamepad input. - * @param {Integer} speedFactor - Times by which the tabbing speed should be increased. - * @param {Boolean} invert - Whether the tabbing should be in opposite order. + * @param {Object} actionOptions - The action options (ex: speedFactor). * */ - gamepad.inputMapperUtils.content.thumbstickTabbing = function (that, value, speedFactor, invert) { - var inversionFactor = invert ? -1 : 1; - value = value * inversionFactor; - clearInterval(that.intervalRecords.forwardTab); - clearInterval(that.intervalRecords.reverseTab); - if (value > 0) { - that.intervalRecords.forwardTab = setInterval( - that.forwardTab, - that.options.frequency * speedFactor, - value - ); - } - else if (value < 0) { - that.intervalRecords.reverseTab = setInterval( - that.reverseTab, - that.options.frequency * speedFactor, - -1 * value - ); + gamepad.inputMapperUtils.content.thumbstickTabbing = function (that, value, actionOptions) { + if (that.model.pageInView) { + var speedFactor = actionOptions.speedFactor || 1; + var inversionFactor = actionOptions.invert ? -1 : 1; + value = value * inversionFactor; + clearInterval(that.intervalRecords.forwardTab); + clearInterval(that.intervalRecords.reverseTab); + if (value > 0) { + that.intervalRecords.forwardTab = setInterval( + that.forwardTab, + that.options.frequency * speedFactor, + value + ); + } + else if (value < 0) { + that.intervalRecords.reverseTab = setInterval( + that.reverseTab, + that.options.frequency * speedFactor, + -1 * value + ); + } } }; @@ -237,7 +280,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * */ gamepad.inputMapperUtils.content.buttonTabNavigation = function (that, value, direction) { - if (value > that.options.cutoffValue) { + if (that.model.pageInView && (value > that.options.cutoffValue)) { var length = that.tabbableElements.length; // Tab only if at least one tabbable element is available. @@ -247,7 +290,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * currently focused, shift the focus to the first element. Otherwise * shift the focus to the next element. */ - var activeElement = document.activeElement; + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; if (activeElement.nodeName === "BODY" || !activeElement) { that.tabbableElements[0].focus(); } @@ -262,20 +305,26 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE activeElementIndex = that.currentTabIndex; } + var increment = 0; if (direction === "forwardTab") { - that.currentTabIndex = (activeElementIndex + 1) % length; + increment = 1; } else if (direction === "reverseTab") { - /** - * Move to the first element if the last element on the webpage - * is focused. - */ - if (activeElementIndex === 0) { - activeElementIndex = length; - } - that.currentTabIndex = activeElementIndex - 1; + increment = -1; + } + + activeElement.blur(); + + that.currentTabIndex = (that.tabbableElements.length + (activeElementIndex + increment)) % that.tabbableElements.length; + var elementToFocus = that.tabbableElements[that.currentTabIndex]; + elementToFocus.focus(); + + // If focus didn't succeed, make one more attempt, to attempt to avoid focus traps (See #118). + if (!that.model.activeModal && elementToFocus !== document.activeElement) { + that.currentTabIndex = (that.tabbableElements.length + (that.currentTabIndex + increment)) % that.tabbableElements.length; + var failoverElementToFocus = that.tabbableElements[that.currentTabIndex]; + failoverElementToFocus.focus(); } - that.tabbableElements[that.currentTabIndex].focus(); } } } @@ -285,57 +334,88 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * Click on the currently focused element. * + * @param {Object} that - The inputMapper component. * @param {Integer} value - The value of the gamepad input. * */ - gamepad.inputMapperUtils.content.click = function (value) { - if (value > 0) { - /** - * If SELECT element is currently focused, toggle its state. Otherwise perform - * the regular click operation. - */ - if (document.activeElement.nodeName === "SELECT") { - var optionsLength = 0; - - // Compute the number of options and store it. - document.activeElement.childNodes.forEach(function (childNode) { - if (childNode.nodeName === "OPTION") { - optionsLength++; - } - }); + gamepad.inputMapperUtils.content.click = function (that, value) { + if (that.model.pageInView && (value > 0)) { + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; - // Toggle the SELECT dropdown. - if (!document.activeElement.getAttribute("size") || document.activeElement.getAttribute("size") === "1") { - /** - * Store the initial size of the dropdown in a separate attribute - * (if specified already). - */ - var initialSizeString = document.activeElement.getAttribute("size"); - if (initialSizeString) { - document.activeElement.setAttribute("initialSize", parseInt(initialSizeString)); + if (activeElement) { + var isTextInput = gamepad.inputMapperUtils.content.isTextInput(activeElement); + + // Open the new onscreen keyboard to input text. + if (isTextInput) { + var lastExternalFocused = activeElement; + that.applier.change("lastExternalFocused", lastExternalFocused); + that.applier.change("textInputValue", lastExternalFocused.value); + lastExternalFocused.blur(); + + that.applier.change("activeModal", "onscreenKeyboard"); + } + /** + * If SELECT element is currently focused, toggle its state. Otherwise perform + * the regular click operation. + */ + else if (activeElement.nodeName === "SELECT") { + var optionsLength = 0; + + // Compute the number of options and store it. + activeElement.childNodes.forEach(function (childNode) { + if (childNode.nodeName === "OPTION") { + optionsLength++; + } + }); + + // Toggle the SELECT dropdown. + if (!activeElement.getAttribute("size") || activeElement.getAttribute("size") === "1") { + /** + * Store the initial size of the dropdown in a separate attribute + * (if specified already). + */ + var initialSizeString = activeElement.getAttribute("size"); + if (initialSizeString) { + activeElement.setAttribute("initialSize", parseInt(initialSizeString)); + } + + /** + * Allow limited expansion to avoid an overflowing list, considering the + * list could go as large as 100 or more (for example, a list of + * countries). + */ + var length = Math.min(15, optionsLength); + activeElement.setAttribute("size", length); } + else { + // Obtain the initial size of the dropdown. + var sizeString = activeElement.getAttribute("initialSize") || "1"; - /** - * Allow limited expansion to avoid an overflowing list, considering the - * list could go as large as 100 or more (for example, a list of - * countries). - */ - var length = Math.min(15, optionsLength); - document.activeElement.setAttribute("size", length); + // Restore the size of the dropdown. + activeElement.setAttribute("size", parseInt(sizeString)); + } } else { - // Obtain the initial size of the dropdown. - var sizeString = document.activeElement.getAttribute("initialSize") || "1"; - - // Restore the size of the dropdown. - document.activeElement.setAttribute("size", parseInt(sizeString)); + // Click on the focused element. + activeElement.click(); } } - else { - // Click on the focused element. - document.activeElement.click(); + } + }; + + gamepad.inputMapperUtils.content.isTextInput = function (element) { + if (element.nodeName === "INPUT") { + var allowedTypes = ["text", "search", "email", "password", "tel", "text", "url"]; + var inputType = element.getAttribute("type"); + if (!inputType || allowedTypes.includes(inputType)) { + return true; } } + else if (element.nodeName === "TEXTAREA") { + return true; + } + + return false; }; /** @@ -344,19 +424,20 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * @param {Object} that - The inputMapper component. * @param {Integer} value - The value of the gamepad input. - * @param {Boolean} invert - Whether the history navigation should be in opposite - * order. + * @param {Object} actionOptions - The action options (ex: invert). * */ - gamepad.inputMapperUtils.content.thumbstickHistoryNavigation = function (that, value, invert) { - // Get the updated input value according to the configuration. - var inversionFactor = invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - that.nextPageInHistory(value); - } - else if (value < 0) { - that.previousPageInHistory(-1 * value); + gamepad.inputMapperUtils.content.thumbstickHistoryNavigation = function (that, value, actionOptions) { + if (that.model.pageInView) { + // Get the updated input value according to the configuration. + var inversionFactor = actionOptions.invert ? -1 : 1; + value = value * inversionFactor; + if (value > 0) { + that.nextPageInHistory(value); + } + else if (value < 0) { + that.previousPageInHistory(-1 * value); + } } }; @@ -374,27 +455,32 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * */ gamepad.inputMapperUtils.content.previousPageInHistory = function (that, value) { - if (value > that.options.cutoffValue) { - var activeElementIndex = null; + if (that.model.pageInView && (value > that.options.cutoffValue)) { + if (window.history.length > 1) { + var activeElementIndex = null; + + // Get the index of the currently active element, if available. + if (fluid.get(document, "activeElement")) { + var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); + activeElementIndex = tabbableElements.indexOf(document.activeElement); + } - // Get the index of the currently active element, if available. - if (fluid.get(document, "activeElement")) { - var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter); - activeElementIndex = tabbableElements.indexOf(document.activeElement); + /** + * Store the index of the active element in local storage object with its key + * set to the URL of the webpage and navigate back in history. + */ + var storageData = {}, + pageAddress = that.options.windowObject.location.href; + if (activeElementIndex !== -1) { + storageData[pageAddress] = activeElementIndex; + } + chrome.storage.local.set(storageData, function () { + that.options.windowObject.history.back(); + }); } - - /** - * Store the index of the active element in local storage object with its key - * set to the URL of the webpage and navigate back in history. - */ - var storageData = {}, - pageAddress = that.options.windowObject.location.href; - if (activeElementIndex !== -1) { - storageData[pageAddress] = activeElementIndex; + else { + that.vibrate(); } - chrome.storage.local.set(storageData, function () { - that.options.windowObject.history.back(); - }); } }; @@ -407,47 +493,61 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * */ gamepad.inputMapperUtils.content.nextPageInHistory = function (that, value) { - if (value > that.options.cutoffValue) { - var activeElementIndex = null; + if (that.model.pageInView && (value > that.options.cutoffValue)) { + if (window.history.length > 1) { + var activeElementIndex = null; + + // Get the index of the currently active element, if available. + if (fluid.get(document, "activeElement")) { + var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); + activeElementIndex = tabbableElements.indexOf(document.activeElement); + } - // Get the index of the currently active element, if available. - if (fluid.get(document, "activeElement")) { - var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter); - activeElementIndex = tabbableElements.indexOf(document.activeElement); + /** + * Store the index of the active element in local storage object with its key + * set to the URL of the webpage and navigate forward in history. + */ + var storageData = {}, + pageAddress = that.options.windowObject.location.href; + if (activeElementIndex !== -1) { + storageData[pageAddress] = activeElementIndex; + } + chrome.storage.local.set(storageData, function () { + that.options.windowObject.history.forward(); + }); } - - /** - * Store the index of the active element in local storage object with its key - * set to the URL of the webpage and navigate forward in history. - */ - var storageData = {}, - pageAddress = that.options.windowObject.location.href; - if (activeElementIndex !== -1) { - storageData[pageAddress] = activeElementIndex; + else { + that.vibrate(); } - chrome.storage.local.set(storageData, function () { - that.options.windowObject.history.forward(); - }); } }; /** * * Simulate a key press (down and up) on the current focused element. + * @param {Object} that - The inputMapper component. * @param {Number} value - The current value of the input (from 0 to 1). - * @param {String} key - The key (ex: `ArrowLeft`) to simulate. + * @param {Object} actionOptions - The options for this action. + * @property {String} key - The key (ex: `ArrowLeft`) to simulate. * */ - gamepad.inputMapperUtils.content.sendKey = function (value, key) { - if (value > 0) { - var keyDownEvent = new KeyboardEvent("keydown", { key: key, code: key, bubbles: true }); - document.activeElement.dispatchEvent(keyDownEvent); + gamepad.inputMapperUtils.content.sendKey = function (that, value, actionOptions) { + var key = fluid.get(actionOptions, "key"); + + // TODO: Make this use the "analogCutoff" preference. + if (that.model.pageInView && (value > that.options.cutoffValue) && (key !== undefined)) { + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; + + if (activeElement) { + var keyDownEvent = new KeyboardEvent("keydown", { key: key, code: key, bubbles: true }); + activeElement.dispatchEvent(keyDownEvent); - // TODO: Test with text inputs and textarea fields to see if - // beforeinput and input are needed. + // TODO: Test with text inputs and textarea fields to see if + // beforeinput and input are needed. - var keyUpEvent = new KeyboardEvent("keyup", { key: key, code: key, bubbles: true }); - document.activeElement.dispatchEvent(keyUpEvent); + var keyUpEvent = new KeyboardEvent("keyup", { key: key, code: key, bubbles: true }); + activeElement.dispatchEvent(keyUpEvent); + } } }; @@ -457,31 +557,33 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * @param {Object} that - The inputMapper component. * @param {Integer} value - The value of the gamepad input. - * @param {Integer} speedFactor - Times by which the tabbing speed should be increased. - * @param {Boolean} invert - Whether the tabbing should be in opposite order. + * @param {Object} actionOptions - The action options (ex: speedFactor). * @param {String} forwardKey - The key/code for the forward arrow (right or down). * @param {String} backwardKey - The key/code for the backward arrow (left or up). * */ - gamepad.inputMapperUtils.content.thumbstickArrows = function (that, value, speedFactor, invert, forwardKey, backwardKey) { - var inversionFactor = invert ? -1 : 1; + gamepad.inputMapperUtils.content.thumbstickArrows = function (that, value, actionOptions, forwardKey, backwardKey) { + var speedFactor = actionOptions.speedFactor || 1; + var inversionFactor = actionOptions.invert ? -1 : 1; value = value * inversionFactor; clearInterval(that.intervalRecords[forwardKey]); clearInterval(that.intervalRecords[backwardKey]); if (value > that.options.cutoffValue) { that.intervalRecords[forwardKey] = setInterval( - gamepad.inputMapperUtils.content.sendKey, - that.options.frequency * speedFactor, - value, - forwardKey + gamepad.inputMapperUtils.content.sendKey, // func + that.options.frequency * speedFactor, // delay + that, // arg 0 + value, //arg 1 + { key: forwardKey } // arg 2 ); } else if (value < (-1 * that.options.cutoffValue)) { that.intervalRecords[backwardKey] = setInterval( gamepad.inputMapperUtils.content.sendKey, that.options.frequency * speedFactor, + that, -1 * value, - backwardKey + { key: backwardKey } ); } }; diff --git a/src/js/content_scripts/input-mapper.js b/src/js/content_scripts/input-mapper.js index 397c348..cd3cc32 100644 --- a/src/js/content_scripts/input-mapper.js +++ b/src/js/content_scripts/input-mapper.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -10,270 +10,298 @@ You may obtain a copy of the BSD 3-Clause License at https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE */ -/* global chrome, ally */ +/* global chrome */ -(function (fluid, $) { +(function (fluid) { "use strict"; var gamepad = fluid.registerNamespace("gamepad"); fluid.registerNamespace("gamepad.inputMapper"); - // TODO: Focus trap in modal, look at Weavly and others. - // TODO: Close on click outside inner container. - // TODO: Close on escape. fluid.defaults("gamepad.inputMapper", { gradeNames: ["gamepad.inputMapper.base", "fluid.viewComponent"], - listeners: { - "onCreate.restoreFocus": "{that}.restoreFocus", - "onCreate.updateControls": "{that}.updateControls" + model: { + activeModal: false, + shadowElement: false, + textInputValue: "", + textInputType: "", + + bindings: gamepad.bindings.defaults, + prefs: gamepad.prefs.defaults }, - invokers: { - // Actions, these are called with: value, speedFactor, invert, background, oldValue, homepageURL - restoreFocus: { - funcName: "gamepad.inputMapper.restoreFocus", - args: ["{that}.options.windowObject", "{that}.tabindexSortFilter"] + modelListeners: { + pageInView: { + func: "gamepad.inputMapper.handlePageInViewChange", + args: ["{that}"] + }, + activeModal: { + func: "{that}.updateTabbables" }, - updateControls: { + textInputValue: { + funcName: "gamepad.inputMapper.updateFormFieldText", + args: ["{that}"] + } + }, + events: { + onWindowFocus: null, + onWindowBlur: null, + onPageShow: null, + onPageHide: null + }, + + listeners: { + // Old persistence. + // TODO: Remove + "onCreate.updateControls": { funcName: "gamepad.inputMapper.updateControls", args: ["{that}"] }, + + "onCreate.loadSettings": { + funcName: "gamepad.inputMapper.loadSettings", + args: ["{that}"] + }, + + // Wire up event listeners to window. + "onCreate.handleWindowFocus": { + funcName: "window.addEventListener", + args: ["focus", "{that}.events.onWindowFocus.fire"] + }, + "onCreate.handleWindowBlur": { + funcName: "window.addEventListener", + args: ["blur", "{that}.events.onWindowBlur.fire"] + }, + "onCreate.handlePageShow": { + funcName: "window.addEventListener", + args: ["pageshow", "{that}.events.onPageShow.fire"] + }, + "onCreate.handlePageHide": { + funcName: "window.addEventListener", + args: ["pagehide", "{that}.events.onPageHide.fire"] + }, + + // Handle window-related events + "onWindowFocus": { + funcName: "gamepad.inputMapper.handleFocused", + args: ["{that}"] + }, + "onPageShow": { + funcName: "gamepad.inputMapper.handleFocused", + args: ["{that}"] + }, + "onWindowBlur": { + funcName: "gamepad.inputMapper.handleBlurred", + args: ["{that}"] + }, + "onPageHide": { + funcName: "gamepad.inputMapper.handleBlurred", + args: ["{that}"] + } + }, + invokers: { + // Actions, these are called with: value, oldValue, actionOptions goToPreviousTab: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "goToPreviousTab", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousTab" }] }, goToNextTab: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "goToNextTab", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextTab"}] }, closeCurrentTab: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "closeCurrentTab", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentTab"}] }, openNewTab: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "openNewTab", "{arguments}.0", "{arguments}.4", "{arguments}.3", "{arguments}.5"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewTab" }] }, openNewWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "openNewWindow", "{arguments}.0", "{arguments}.4", "{arguments}.3", "{arguments}.5"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewWindow" }] }, closeCurrentWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "closeCurrentWindow", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentWindow" }] }, goToPreviousWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "goToPreviousWindow", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousWindow" }] }, goToNextWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "goToNextWindow", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextWindow" }] }, zoomIn: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "zoomIn", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "zoomIn" }] }, zoomOut: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "zoomOut", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "zoomOut" }] }, thumbstickZoom: { funcName: "gamepad.inputMapperUtils.background.thumbstickZoom", args: ["{that}", "{arguments}.0", "{arguments}.2"] }, maximizeWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "maximizeWindow", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "maximizeWindow" }] }, restoreWindowSize: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "restoreWindowSize", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "restoreWindowSize" }] }, thumbstickWindowSize: { funcName: "gamepad.inputMapperUtils.background.thumbstickWindowSize", - args: ["{that}", "{arguments}.0", "{arguments}.2"] + args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, actionOptions }, reopenTabOrWindow: { - funcName: "gamepad.inputMapperUtils.background.sendMessage", - args: ["{that}", "reopenTabOrWindow", "{arguments}.0", "{arguments}.4"] + funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", + args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "reopenTabOrWindow" }] }, openActionLauncher: { funcName: "gamepad.inputMapper.openActionLauncher", - args: ["{that}", "{arguments}.0", "{arguments}.4"] // value, oldValue + args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue + }, + openSearchKeyboard: { + funcName: "gamepad.inputMapper.openSearchKeyboard", + args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue + }, + openConfigPanel: { + funcName: "gamepad.inputMapper.openConfigPanel", + args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue } }, - model: { - hideActionPanelLauncher: true - }, components: { - actionLauncher: { + modalManager: { container: "{that}.container", - type: "gamepad.actionLauncher", + type: "gamepad.modalManager", options: { model: { - hidden: "{gamepad.inputMapper}.model.hideActionPanelLauncher", - lastExternalFocused: "{gamepad.inputMapper}.model.lastExternalFocused" + activeModal: "{gamepad.inputMapper}.model.activeModal", + lastExternalFocused: "{gamepad.inputMapper}.model.lastExternalFocused", + shadowElement: "{gamepad.inputMapper}.model.shadowElement", + textInputValue: "{gamepad.inputMapper}.model.textInputValue", + textInputType: "{gamepad.inputMapper}.model.textInputType" } } } } }); - gamepad.inputMapper.openActionLauncher = function (that, value, oldValue) { - if (value && !oldValue) { - that.applier.change("lastExternalFocused", document.activeElement); - - that.applier.change("hideActionPanelLauncher", false); - } + gamepad.inputMapper.handleFocused = function (that) { + that.applier.change("pageInView", true); }; - /** - * - * Restore the previously focused element on the web page after history navigation or - * switching tabs/windows. - * - * @param {Object} windowObject - The inputMapper component's windowObject option. - * @param {Function} tabindexSortFilter - The filter to be used for sorting elements - * based on their tabindex value. - * - */ - gamepad.inputMapper.restoreFocus = function (windowObject, tabindexSortFilter) { - $(document).ready(function () { - /** - * Get the index of the previously focused element stored in the local - * storage. - */ - var pageAddress = windowObject.location.href; - chrome.storage.local.get([pageAddress], function (resultObject) { - // Focus only if some element was focused before the history navigation. - var activeElementIndex = resultObject[pageAddress]; - if (activeElementIndex && activeElementIndex !== -1) { - var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(tabindexSortFilter), - activeElement = tabbableElements[activeElementIndex]; - if (activeElement) { - activeElement.focus(); - } + gamepad.inputMapper.handleBlurred = function (that) { + that.applier.change("pageInView", false); + }; - // Clear the stored index of the active element after usage. - chrome.storage.local.remove([pageAddress]); - } - }); - }); + gamepad.inputMapper.updateFormFieldText = function (that) { + if (that.model.lastExternalFocused && gamepad.inputMapperUtils.content.isTextInput(that.model.lastExternalFocused)) { + that.model.lastExternalFocused.value = that.model.textInputValue; + } }; - /** - * - * Tracks the page/window visibility state and calls the inputMapper instance manager - * accordingly. - * - * @param {Function} inputMapperManager - The function that handles the instance of - * the inputMapper component. - * @param {Object} configurationOptions - The configuration options for the - * inputMapper component. - * - */ - gamepad.visibilityChangeTracker = (function (windowObject) { - // Assume that the page isn't focused initially. - var inView = false; - return function (inputMapperManager, configurationOptions) { - configurationOptions = configurationOptions || {}; - - // Track changes to the focus/visibility of the window object. - windowObject.onfocus = windowObject.onblur = windowObject.onpageshow = windowObject.onpagehide = function (event) { - /** - * Call the inputMapper instance manager according to the visibility - * status of the page/window and update the inView value. - */ - if (event.type === "focus" || event.type === "pageshow") { - /** - * Call the inputMapper instance manager with the "visible" status if - * the page/window is focused back after switching or when page loads - * (using inView to verify). - */ - if (!inView) { - inputMapperManager("visible", configurationOptions); - inView = true; - } - } - else if (inView) { - /** - * Otherwise, call the inputMapper instance manager with the "hidden" - * status when the focus/visibility of current window/tab is moved to - * some other window/tab. - */ - inputMapperManager("hidden"); - inView = false; - } - }; - }; - })(window); + gamepad.inputMapper.generateModalOpenFunction = function (modalKey) { + return function (that, value, oldValue) { + if (that.model.pageInView && value && !oldValue) { + // In this case we don't want to fail over to a modal's activeElement. + that.applier.change("lastExternalFocused", document.activeElement); - // TODO: Make this something the input mapper does itself to decide whether - // it should be active. When not visible, deactivate all modals and stop - // listening to gamepad input. When visible, start listening. See if there - // is some way to keep it alive if the window is visible but the dev tools - // are focused. - /** - * - * Manages the inputMapper instance according to the visibility status of the - * tab/window. - * - * @param {String} visibilityStatus - The visibility status of the tab/window. - * @param {Object} configurationOptions - The configuration options for the - * inputMapper component. - * - */ - gamepad.inputMapperManager = (function () { - var inputMapperInstance = null; - - return function (visibilityStatus, configurationOptions) { - configurationOptions = configurationOptions || {}; - - /** - * Create an instance of the inputMapper when the tab/window is focused - * again and start reading gamepad inputs (if any gamepad is connected). - */ - if (visibilityStatus === "visible") { - /** - * TODO: Refactor the approach to use a dynamic component instead of - * creating components dynamically. - */ - - // Pass the configuration options to the inputMapper component. - // TODO: Reactivate a "dormant" inputMapper instance here. - inputMapperInstance = gamepad.inputMapper("body", configurationOptions); - inputMapperInstance.events.onGamepadConnected.fire(); - } - else if (visibilityStatus === "hidden" && inputMapperInstance !== null) { - /** - * Destroy the instance of the inputMapper in the current tab when another - * window/tab is focused or opened. - */ - // TODO: Manage this better, it makes inspecting HTML really hard. - // TODO: Make the input mapper "dormant". - inputMapperInstance.destroy(); + that.applier.change("activeModal", modalKey); } }; - })(); + }; + // We do this in a funky way because of our fixed method signature across actions. + gamepad.inputMapper.openActionLauncher = gamepad.inputMapper.generateModalOpenFunction("actionLauncher"); + gamepad.inputMapper.openSearchKeyboard = gamepad.inputMapper.generateModalOpenFunction("searchKeyboard"); + + gamepad.inputMapper.openConfigPanel = function (that, value, oldValue) { + if (oldValue === 0 && value > that.model.prefs.analogCutoff) { + gamepad.inputMapperUtils.background.postMessage(that, { actionName: "openOptionsPage"}); + } + }; + + gamepad.inputMapper.handlePageInViewChange = function (that) { + if (that.model.pageInView) { + gamepad.inputMapper.updateControls(that); + } + else { + that.applier.change("activeModal", false); + } + }; + + // TODO: Remove this once the new bindings are fully wired up. /** * - * Update the gamepad configuration if a custom configuration is available. + * (Re)load the gamepad configuration from local storage. * * @param {Object} that - The inputMapper component. * */ gamepad.inputMapper.updateControls = function (that) { - chrome.storage.local.get(["gamepadConfiguration"], function (configWrapper) { - var gamepadConfig = configWrapper.gamepadConfiguration; + if (that.model.pageInView) { + chrome.storage.local.get(["gamepadConfiguration"], function (configWrapper) { + var gamepadConfig = configWrapper.gamepadConfiguration; + + // Update the gamepad configuration only if it's available. + if (gamepadConfig) { + that.applier.change("map", gamepadConfig); + } + }); + } + }; + + gamepad.inputMapper.loadSettings = async function (that) { + gamepad.inputMapper.loadPrefs(that); + + gamepad.inputMapper.loadBindings(that); - // Update the gamepad configuration only if it's available. - if (gamepadConfig) { - that.applier.change("map", gamepadConfig); + /* + The two params for the onChanged listener callbak are "changes" and "areaName". In our case, "areaName" is + always "local", so we ignore it. "changes" is an object with an entry for each changed key, as in: + + { "gamepad-prefs": newValue: {}} + */ + + chrome.storage.onChanged.addListener(function (changes) { + if (changes["gamepad-prefs"]) { + gamepad.inputMapper.loadPrefs(that); + } + + if (changes["gamepad-bindings"]) { + gamepad.inputMapper.loadBindings(that); } }); }; - gamepad.visibilityChangeTracker(gamepad.inputMapperManager); + gamepad.inputMapper.loadPrefs = async function (that) { + var storedPrefs = await gamepad.utils.getStoredKey("gamepad-prefs"); + var prefsToSave = storedPrefs || gamepad.prefs.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "prefs", value: prefsToSave }); + + transaction.commit(); + }; + + gamepad.inputMapper.loadBindings = async function (that) { + var storedBindings = await gamepad.utils.getStoredKey("gamepad-bindings"); + var bindingsToSave = storedBindings || gamepad.bindings.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "bindings", value: bindingsToSave }); + + transaction.commit(); + }; + + gamepad.inputMapperInstance = gamepad.inputMapper("body"); })(fluid, jQuery); diff --git a/src/js/content_scripts/modal.js b/src/js/content_scripts/modal.js index 9a0ec45..d490afb 100644 --- a/src/js/content_scripts/modal.js +++ b/src/js/content_scripts/modal.js @@ -23,9 +23,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE fluid.defaults("gamepad.modal", { gradeNames: ["gamepad.templateRenderer"], model: { - hidden: true, - lastExternalFocused: false + classNames: "", + hidden: true }, + icon: gamepad.svg["gamepad-icon"], modelListeners: { hidden: [ { @@ -43,18 +44,18 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE icon: ".modal-icon", innerContainer: ".modal-inner-container", modalBody: ".modal-body", + modalFooter: ".modal-footer", modalCloseButton: ".modal-close-button", leadingFocusTrap: ".modal-focus-trap-leading", trailingFocusTrap: ".modal-focus-trap-trailing" }, markup: { - // TODO: Add the ability to retrieve our icon URL and display the icon onscreen. - container: "" + container: "" }, invokers: { closeModal: { funcName: "gamepad.modal.closeModal", - args: ["{that}", "{arguments}.0"] // event + args: ["{gamepad.modalManager}", "{arguments}.0"] // event }, handleKeydown: { funcName: "gamepad.modal.handleKeydown", @@ -74,15 +75,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE args: ["{that}", true] } }, - // TODO: Relay hidden classname to control visibility. - // TODO: clicking outside the inner container should close the modal. - // We can't use this form because we also need to restore focus. - // modelRelay: { - // source: "{that}.model.dom.outerContainer.click", - // target: "{that}.model.hidden", - // singleTransform: "fluid.transforms.toggle" - // }, listeners: { + "onCreate.drawIcon": { + funcName: "gamepad.modal.drawIcon", + args: ["{that}"] + }, "onCreate.bindOuterContainerClick": { this: "{that}.container", method: "click", @@ -134,11 +131,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } }; - gamepad.modal.closeModal = function (that, event) { + gamepad.modal.drawIcon = function (that) { + var iconElement = that.locate("icon"); + iconElement.html(that.options.icon); + }; + + /** + * + * @param {Object} modalManager - The modal manager component. + * @param {Event} event - The event to which we are responding. + */ + gamepad.modal.closeModal = function (modalManager, event) { event.preventDefault(); - that.applier.change("hidden", true); - if (that.model.lastExternalFocused && that.model.lastExternalFocused.focus) { - that.model.lastExternalFocused.focus(); + modalManager.applier.change("activeModal", false); + if (modalManager.model.lastExternalFocused && modalManager.model.lastExternalFocused.focus) { + modalManager.model.lastExternalFocused.focus(); } }; @@ -156,8 +163,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE var innerContainer = that.locate("innerContainer"); // Search for tabbables, focus on first or last element depending. - // TODO: Repurpose sorting from other areas where we use ally? - var tabbableElements = ally.query.tabbable({ context: innerContainer, strategy: "strict" }); + var tabbableElements = ally.query.tabsequence({ context: innerContainer, strategy: "strict" }); if (tabbableElements.length) { var elementIndex = reverse ? tabbableElements.length - 1 : 0; var elementToFocus = tabbableElements[elementIndex]; diff --git a/src/js/content_scripts/modalManager.js b/src/js/content_scripts/modalManager.js new file mode 100644 index 0000000..d4d24e7 --- /dev/null +++ b/src/js/content_scripts/modalManager.js @@ -0,0 +1,112 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + + + fluid.defaults("gamepad.modalManager", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "
", + styles: "" + }, + model: { + activeModal: false, + shadowElement: false, + lastExternalFocused: false, + textInputValue: "", + textInputType: "", + + // Inline all styles from JS-wrapped global namespaced variable. + styles: gamepad.css + }, + events: { + onShadowReady: null + }, + components: { + actionLauncher: { + container: "{that}.model.shadowElement", + type: "gamepad.actionLauncher", + createOnEvent: "onShadowReady", + options: { + model: { + hidden: "{gamepad.modalManager}.model.hideActionLauncher" + } + } + }, + onscreenKeyboard: { + container: "{that}.model.shadowElement", + type: "gamepad.osk.modal", + createOnEvent: "onShadowReady", + options: { + model: { + hidden: "{gamepad.modalManager}.model.hideOnscreenKeyboard", + textInputValue: "{gamepad.modalManager}.model.textInputValue" + } + } + }, + searchKeyboard: { + container: "{that}.model.shadowElement", + type: "gamepad.searchKeyboard.modal", + createOnEvent: "onShadowReady", + options: { + model: { + hidden: "{gamepad.modalManager}.model.hideSearchKeyboard" + } + } + } + }, + listeners: { + "onCreate.createShadow": { + funcName: "gamepad.modalManager.createShadow", + args: ["{that}"] + } + }, + modelListeners: { + activeModal: { + excludeSource: "init", + funcName: "gamepad.modalManager.toggleModals", + args: ["{that}"] + } + } + }); + + gamepad.modalManager.createShadow = function (that) { + var host = that.container[0]; + var shadowElement = host.attachShadow({mode: "open"}); + + // We inline all styles here so that all modals get the common styles, + // and to avoid managing multiple shadow elements. + shadowElement.innerHTML = fluid.stringTemplate(that.options.markup.styles, that.model); + + that.applier.change("shadowElement", shadowElement); + that.events.onShadowReady.fire(); + }; + + gamepad.modalManager.toggleModals = function (that) { + var transaction = that.applier.initiate(); + var hideActionLauncher = that.model.activeModal !== "actionLauncher"; + transaction.fireChangeRequest({ path: "hideActionLauncher", value: hideActionLauncher }); + + var hideOnscreenKeyboard = that.model.activeModal !== "onscreenKeyboard"; + transaction.fireChangeRequest({ path: "hideOnscreenKeyboard", value: hideOnscreenKeyboard }); + + + var hideSearchKeyboard = that.model.activeModal !== "searchKeyboard"; + transaction.fireChangeRequest({ path: "hideSearchKeyboard", value: hideSearchKeyboard }); + + transaction.commit(); + }; +})(fluid); diff --git a/src/js/content_scripts/onscreen-keyboard.js b/src/js/content_scripts/onscreen-keyboard.js new file mode 100644 index 0000000..e26bf9d --- /dev/null +++ b/src/js/content_scripts/onscreen-keyboard.js @@ -0,0 +1,119 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + // Adapted from: https://github.com/duhrer/fluid-osk/blob/GH-1/examples/js/customTextInput.js + + fluid.defaults("gamepad.osk.modal", { + gradeNames: ["gamepad.modal"], + model: { + classNames: " onscreen-keyboard-modal", + label: "Gamepad Navigator: Onscreen Keyboard", + lastExternalFocused: false, + textInputValue: "" + }, + components: { + input: { + container: "{that}.dom.modalBody", + type: "osk.inputs.text", + options: { + model: { + composition: "{gamepad.osk.modal}.model.textInputValue" + }, + modelListeners: { + composition: { + excludeSource: "local", + funcName: "gamepad.osk.modal.updateTextInputWithExternalValue", + args: ["{that}"] + } + } + } + }, + osk: { + container: "{that}.dom.modalBody", + type: "gamepad.osk.keyboard", + options: { + listeners: { + "onAction.updateInput": { + priority: "before:handleLatches", + funcName: "gamepad.osk.modal.processAction", + args: ["{that}", "{osk.inputs.text}", "{arguments}.0"] // inputComponent, actionDef + } + } + } + } + } + }); + + // Our assumptions are slightly different than fluid-osk, so we hack in an override to pass in external values. + // It may be possible to move this to fluid-osk, but in testing this causes problems with other external updates, + // i.e. those made indirectly via addChar and other invokers. + gamepad.osk.modal.updateTextInputWithExternalValue = function (inputComponent) { + var transaction = inputComponent.applier.initiate(); + transaction.fireChangeRequest({ path: "cursorIndex", value: inputComponent.model.composition.length}); + transaction.fireChangeRequest({ path: "beforeCursor", value: inputComponent.model.composition }); + transaction.fireChangeRequest({ path: "afterCursor", value: "" }); + transaction.commit(); + }; + + gamepad.osk.modal.processAction = function (keyboardComponent, inputComponent, actionDef) { + if (actionDef.action === "text") { + var toAdd = actionDef.payload; + if (keyboardComponent.model.latchedKeys.ShiftLeft || keyboardComponent.model.latchedKeys.ShiftRight ) { + toAdd = actionDef.shiftPayload; + } + else if (keyboardComponent.model.latchedKeys.CapsLock) { + toAdd = actionDef.capsPayload; + } + var isInsert = keyboardComponent.model.latchedKeys.Insert; + inputComponent.addChar(toAdd, isInsert); + } + else if (actionDef.action === "backspace") { + inputComponent.removePreviousChar(); + } + else if (actionDef.action === "delete") { + inputComponent.removeNextChar(); + } + else if (actionDef.action === "up" || actionDef.action === "left") { + inputComponent.moveCursor(-1); + } + else if (actionDef.action === "down" || actionDef.action === "right") { + inputComponent.moveCursor(1); + } + else if (actionDef.action === "home") { + inputComponent.moveCursorToStart(); + } + else if (actionDef.action === "end") { + inputComponent.moveCursorToEnd(); + } + }; + + // Wrapped in case we want to extend later, for example to add bespoke + // buttons for domains. + fluid.defaults("gamepad.osk.keyboard", { + gradeNames: ["osk.keyboard.qwerty"] + }); +})(fluid); diff --git a/src/js/content_scripts/search-keyboard.js b/src/js/content_scripts/search-keyboard.js new file mode 100644 index 0000000..cd6c0c9 --- /dev/null +++ b/src/js/content_scripts/search-keyboard.js @@ -0,0 +1,81 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.searchKeyboard.searchButton", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "" + } + }); + + fluid.defaults("gamepad.searchKeyboard.modal", { + gradeNames: ["gamepad.osk.modal"], + model: { + label: "Gamepad Navigator: Search", + classNames: " gamepad-navigator-searchKeyboard" + }, + + invokers: { + handleSearchButtonClick: { + funcName: "gamepad.searchKeyboard.modal.handleSearchButtonClick", + args: ["{that}", "{inputMapper}", "{arguments}.0"] + } + }, + + components: { + searchButton: { + container: "{that}.dom.modalFooter", + type: "gamepad.searchKeyboard.searchButton", + options: { + listeners: { + "onCreate.bindClick": { + this: "{searchButton}.container", + method: "click", + args: ["{gamepad.searchKeyboard.modal}.handleSearchButtonClick"] + } + } + } + } + } + }); + + gamepad.searchKeyboard.modal.handleSearchButtonClick = function (that, inputMapper, event) { + that.applier.change("activeModal", false); + event.preventDefault(); + + if (that.model.textInputValue && that.model.textInputValue.trim().length) { + var actionOptions = { + actionName: "search", + // See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/search/query + disposition: "NEW_TAB", + text: that.model.textInputValue.trim() + }; + + gamepad.inputMapperUtils.background.postMessage(inputMapper, actionOptions); + } + }; +})(fluid); diff --git a/src/js/shared/templateRenderer.js b/src/js/content_scripts/templateRenderer.js similarity index 72% rename from src/js/shared/templateRenderer.js rename to src/js/content_scripts/templateRenderer.js index 227eaab..31e2d94 100644 --- a/src/js/shared/templateRenderer.js +++ b/src/js/content_scripts/templateRenderer.js @@ -6,6 +6,18 @@ https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. Licensed under the BSD 3-Clause License. You may not use this file except in compliance with this License. +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + You may obtain a copy of the BSD 3-Clause License at https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE */ diff --git a/src/js/content_scripts/utils.js b/src/js/content_scripts/utils.js new file mode 100644 index 0000000..9e6178b --- /dev/null +++ b/src/js/content_scripts/utils.js @@ -0,0 +1,83 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +/* globals chrome */ +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + fluid.registerNamespace("gamepad.utils"); + + gamepad.utils.isDeeplyEqual = function (firstThing, secondThing) { + if (typeof firstThing !== typeof secondThing) { + return false; + } + else if (Array.isArray(firstThing)) { + if (firstThing.length === secondThing.length) { + for (var arrayIndex = 0; arrayIndex < firstThing.length; arrayIndex++) { + var arrayItemsEqual = gamepad.utils.isDeeplyEqual(firstThing[arrayIndex], secondThing[arrayIndex]); + if (!arrayItemsEqual) { return false; } + } + return true; + } + else { + return false; + } + } + else if (typeof firstThing === "object") { + var firstThingKeys = Object.keys(firstThing); + var secondThingKeys = Object.keys(secondThing); + + if (firstThingKeys.length !== secondThingKeys.length) { + return false; + } + + for (var keyIndex = 0; keyIndex < firstThingKeys.length; keyIndex++) { + var key = firstThingKeys[keyIndex]; + var objectPropertiesEqual = gamepad.utils.isDeeplyEqual(firstThing[key], secondThing[key]); + if (!objectPropertiesEqual) { return false; } + } + + return true; + } + else { + return firstThing === secondThing; + } + }; + + gamepad.utils.getStoredKey = function (key) { + var storagePromise = new Promise(function (resolve) { + // Apparently, we have to use a callback and can't use `await` unless we make our own wrapper. + chrome.storage.local.get([key], function (storedObject) { + if (storedObject[key]) { + resolve(storedObject[key]); + } + else { + resolve(false); + } + }); + }); + return storagePromise; + }; + +})(fluid); diff --git a/src/js/settings/settings.js b/src/js/settings/settings.js new file mode 100644 index 0000000..a0e37e2 --- /dev/null +++ b/src/js/settings/settings.js @@ -0,0 +1,441 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +/* globals chrome */ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + fluid.defaults("gamepad.settings", { + gradeNames: ["gamepad.templateRenderer"], + injectionType: "replaceWith", + markup: { + container: "
" + }, + model: { + prefs: gamepad.prefs.defaults, + bindings: gamepad.bindings.defaults + }, + modelListeners: { + prefs: { + excludeSource: "init", + funcName: "gamepad.settings.savePrefs", + args: ["{that}.model.prefs"] + }, + bindings: { + excludeSource: "init", + funcName: "gamepad.settings.saveBindings", + args: ["{that}.model.bindings"] + } + }, + listeners: { + "onCreate.loadSettings": { + funcName: "gamepad.settings.loadSettings", + args: ["{that}"] + } + }, + components: { + prefs: { + container: "{that}.container", + type: "gamepad.config.prefs", + options: { + model: { + prefs: "{gamepad.settings}.model.prefs" + } + } + } + // buttonBindings: { + // container: "{that}.dom.body", + // type: "gamepad.config.bindings", + // options: { + // model: { + // label: "Buttons / Triggers", + // bindings: "{gamepad.settings}.model.bindings.buttons" + // } + // } + // }, + // axisBindings: { + // container: "{that}.dom.body", + // type: "gamepad.config.bindings", + // options: { + // model: { + // label: "Axes (Thumb sticks)", + // bindings: "{gamepad.settings}.model.bindings.axes" + // } + // } + // } + } + }); + + gamepad.settings.loadSettings = async function (that) { + gamepad.settings.loadPrefs(that); + + gamepad.settings.loadBindings(that); + + // In the similar function in input mapper, we add a listener for changes to values in local storage. As we + // have code to ensure that there is only open settings panel, and since only the settings panel can update + // stored values, we should safely be able to avoid listening for local storage changes here. + }; + + gamepad.settings.loadPrefs = async function (that) { + var storedPrefs = await gamepad.utils.getStoredKey("gamepad-prefs"); + var prefsToSave = storedPrefs || gamepad.prefs.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "prefs", value: prefsToSave }); + + transaction.commit(); + }; + + gamepad.settings.loadBindings = async function (that) { + var storedBindings = await gamepad.utils.getStoredKey("gamepad-bindings"); + var bindingsToSave = storedBindings || gamepad.bindings.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "bindings", value: bindingsToSave }); + + transaction.commit(); + }; + + gamepad.settings.savePrefs = function (prefs) { + var prefsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.prefs.defaults, prefs); + if (prefsEqualDefaults) { + chrome.storage.local.remove("gamepad-prefs"); + } + else { + chrome.storage.local.set({ "gamepad-prefs": prefs }); + } + }; + + gamepad.settings.saveBindings = async function (bindings) { + var bindingsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.bindings.defaults, bindings); + if (bindingsEqualDefaults) { + chrome.storage.local.remove("gamepad-bindings"); + } + else { + chrome.storage.local.set({ "gamepad-bindings": bindings }); + } + }; + + fluid.defaults("gamepad.config.draftHandlingButton", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "" + }, + model: { + label: "Draft Button", + disabled: true + }, + modelRelay: { + source: "{that}.model.disabled", + target: "{that}.model.dom.container.attr.disabled" + } + }); + + fluid.defaults("gamepad.config.draftHandlingButton.discard", { + gradeNames: ["gamepad.config.draftHandlingButton"], + model: { + label: "Discard Changes" + } + }); + + fluid.defaults("gamepad.config.draftHandlingButton.save", { + gradeNames: ["gamepad.config.draftHandlingButton"], + model: { + label: "Save Changes" + } + }); + + + fluid.defaults("gamepad.config.editableSection", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "

%label

" + }, + selectors: { + body: ".gamepad-config-editable-section-body", + footer: ".gamepad-config-editable-section-footer" + }, + model: { + label: "Editable Section", + draftClean: true + }, + components: { + discardButton: { + container: "{that}.dom.footer", + type: "gamepad.config.draftHandlingButton.discard", + options: { + model: { + disabled: "{gamepad.config.editableSection}.model.draftClean" + }, + listeners: { + "onCreate.bindClick": { + this: "{gamepad.config.draftHandlingButton}.container", + method: "click", + args: ["{gamepad.config.editableSection}.resetDraft"] + } + } + } + }, + saveButton: { + container: "{that}.dom.footer", + type: "gamepad.config.draftHandlingButton.save", + options: { + model: { + disabled: "{gamepad.config.editableSection}.model.draftClean" + }, + listeners: { + "onCreate.bindClick": { + this: "{gamepad.config.draftHandlingButton}.container", + method: "click", + args: ["{gamepad.config.editableSection}.saveDraft"] + } + } + } + } + }, + invokers: { + saveDraft: { + funcName: "fluid.notImplemented" + }, + resetDraft: { + funcName: "fluid.notImplemented" + } + } + }); + + /* + analogCutoff: 0.25, // was 0.4 + + newTabOrWindowURL: "https://www.google.com/", + + openWindowOnStartup: true, + vibrate: true + */ + + fluid.defaults("gamepad.config.prefs", { + gradeNames: ["gamepad.config.editableSection"], + model: { + label: "Preferences", + prefs: {}, + draftPrefs: {} + }, + modelRelay: { + source: "prefs", + target: "draftPrefs", + backward: { + excludeSource: "*" + } + }, + modelListeners: { + prefs: { + excludeSource: "init", + funcName: "gamepad.config.prefs.resetDraft", + args: ["{that}"] + + }, + draftPrefs: { + excludeSource: "local", + funcName: "gamepad.config.prefs.flagDraftChanged", + args: ["{that}"] + } + }, + invokers: { + saveDraft: { + funcName: "gamepad.config.prefs.saveDraft", + args: ["{that}"] + }, + resetDraft: { + funcName: "gamepad.config.prefs.resetDraft", + args: ["{that}"] + } + }, + components: { + vibrate: { + container: "{that}.dom.body", + type: "gamepad.ui.toggle", + options: { + model: { + label: "Vibrate", + checked: "{gamepad.config.prefs}.model.draftPrefs.vibrate" + } + } + }, + openWindowOnStartup: { + container: "{that}.dom.body", + type: "gamepad.ui.toggle", + options: { + model: { + label: "Open Settings on Startup", + checked: "{gamepad.config.prefs}.model.draftPrefs.openWindowOnStartup" + } + } + } + } + }); + + gamepad.config.prefs.resetDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "draftPrefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "draftPrefs", value: that.model.prefs }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + gamepad.config.prefs.flagDraftChanged = function (that) { + var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftPrefs, that.model.prefs); + that.applier.change("draftClean", draftClean); + }; + + gamepad.config.prefs.saveDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "prefs", value: that.model.draftPrefs }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + /* + Existing options we need to display/edit (and actions that use them`) + speedFactorOption: [ + "reverseTab", + "forwardTab", + "scrollLeft", + "scrollRight", + "scrollUp", + "scrollDown", + "scrollHorizontally", + "scrollVertically", + "thumbstickTabbing", + "thumbstickHorizontalArrows", + "thumbstickVerticalArrows" + ], + backgroundOption: ["openNewTab", "openNewWindow"], + invertOption: [ + "scrollHorizontally", + "scrollVertically", + "thumbstickHistoryNavigation", + "thumbstickTabbing", + "thumbstickZoom", + "thumbstickWindowSize", + "thumbstickHorizontalArrows", + "thumbstickVerticalArrows" + ] + + In addition, we need to make the existing hard-coded "frequency" option configurable for everything except: + + key: "click", + key: "openConfigPanel", + key: "openSearchKeyboard", + key: "openNewWindow", + key: "openNewTab", + key: "maximizeWindow", + key: "restoreWindowSize", + + // On the fence, but on balance I'd rather they not be repeatable. + key: "closeCurrentTab", + key: "closeCurrentWindow", + key: "reopenTabOrWindow", + key: "previousPageInHistory", + key: "nextPageInHistory", + + These should be repeatable: + + key: "goToPreviousWindow", + key: "goToNextWindow", + key: "goToPreviousTab", + key: "goToNextTab", + + key: "reverseTab", + key: "forwardTab", + key: "scrollLeft", + key: "scrollRight", + key: "scrollUp", + key: "scrollDown", + key: "zoomIn", + key: "zoomOut", + key: "sendArrowLeft", + key: "sendArrowRight", + key: "sendArrowUp", + key: "sendArrowDown", + + */ + + // Component to edit a section of the bindings, i.e. only axes or buttons. + fluid.defaults("gamepad.config.bindings", { + gradeNames: ["gamepad.config.editableSection"], + model: { + label: "Bindings", + bindings: {}, + draftBindings: "{that}.model.bindings" + }, + modelListeners: { + bindings: { + excludeSource: "init", + funcName: "gamepad.config.prefs.resetDraft", + args: ["{that}"] + + }, + draftBindings: { + excludeSource: "local", + funcName: "gamepad.config.prefs.flagDraftChanged", + args: ["{that}"] + } + }, + invokers: { + saveDraft: { + funcName: "gamepad.config.prefs.saveDraft", + args: ["{that}"] + }, + resetDraft: { + funcName: "gamepad.config.prefs.resetDraft", + args: ["{that}"] + } + } + }); + + gamepad.config.bindings.resetDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "draftBindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "draftBindings", value: that.model.bindings }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + gamepad.config.bindings.flagDraftChanged = function (that) { + var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftBindings, that.model.bindings); + that.applier.change("draftClean", draftClean); + }; + + gamepad.config.bindings.saveDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "bindings", value: that.model.draftBindings }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + // TODO: As long as there are unbound buttons/axes, present an "add binding" + // form at the bottom of each list. + + gamepad.settings(".gamepad-settings-body"); +})(fluid); diff --git a/src/js/settings/toggle.js b/src/js/settings/toggle.js new file mode 100644 index 0000000..b76caca --- /dev/null +++ b/src/js/settings/toggle.js @@ -0,0 +1,45 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + // var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.ui.toggle", { + gradeNames: ["gamepad.templateRenderer"], + styles: { + checked: "checked" + }, + model: { + label: "Toggle", + checked: true + }, + markup: { + container: "
%label
" + }, + selectors: { + toggle: ".gamepad-toggle" + }, + modelRelay: [ + { + source: "{that}.model.dom.toggle.click", + target: "{that}.model.checked", + singleTransform: "fluid.transforms.toggle" + }, + { + source: "{that}.model.checked", + target: { + segs: ["dom", "toggle", "class", "{that}.options.styles.checked"] + } + } + ] + }); +})(fluid); diff --git a/src/js/shared/configuration-maps.js b/src/js/shared/configuration-maps.js index 4b8ba8e..2da88f6 100644 --- a/src/js/shared/configuration-maps.js +++ b/src/js/shared/configuration-maps.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/src/js/shared/prefs.js b/src/js/shared/prefs.js new file mode 100644 index 0000000..58bbd1f --- /dev/null +++ b/src/js/shared/prefs.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ + +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + fluid.registerNamespace("gamepad.prefs"); + gamepad.prefs.defaults = { + analogCutoff: 0.25, + newTabOrWindowURL: "https://www.google.com/", + openWindowOnStartup: true, + vibrate: true + }; +})(fluid); diff --git a/src/manifest.json b/src/manifest.json index 23ee38a..649d790 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,32 +1,44 @@ { "name": "Gamepad Navigator", - "version": "0.3.0", + "version": "1.0.0", "description": "A Chrome extension that allows you to navigate web pages and Chromium-based browsers using a game controller.", - "author": "Divyanshu Mahajan", + "author": "The Gamepad Navigator Authors", "manifest_version": 3, "permissions": [ "storage", "tabs", - "sessions" + "sessions", + "search" ], "content_scripts": [ { "matches": [ "" ], - "css": [ - "css/common.css", - "css/modal.css", - "css/action-launcher.css" - ], + "css": [], "js": [ "js/lib/infusion/infusion-all.js", "js/lib/ally/ally.min.js", + "js/lib/fluid-osk/templateRenderer.js", + "js/lib/fluid-osk/keydefs.js", + "js/lib/fluid-osk/key.js", + "js/lib/fluid-osk/row.js", + "js/lib/fluid-osk/keyboard.js", + "js/lib/fluid-osk/keyboards.js", + "js/lib/fluid-osk/inputs.js", + "js/content_scripts/utils.js", "js/content_scripts/gamepad-navigator.js", "js/shared/configuration-maps.js", - "js/shared/templateRenderer.js", + "js/shared/prefs.js", + "js/content_scripts/bindings.js", + "js/content_scripts/styles.js", + "js/content_scripts/svgs.js", + "js/content_scripts/templateRenderer.js", "js/content_scripts/modal.js", "js/content_scripts/action-launcher.js", + "js/content_scripts/onscreen-keyboard.js", + "js/content_scripts/search-keyboard.js", + "js/content_scripts/modalManager.js", "js/content_scripts/input-mapper-content-utils.js", "js/content_scripts/input-mapper-background-utils.js", "js/content_scripts/input-mapper-base.js", @@ -44,15 +56,16 @@ "128": "images/gamepad-icon-128px.png" }, "action": { - "default_icon": "images/gamepad-icon.png", - "default_popup": "html/configuration-panel.html", - "default_title": "Open the gamepad configuration panel." + "default_icon": { + "16": "images/gamepad-icon-16px.png", + "32": "images/gamepad-icon-32px.png", + "48": "images/gamepad-icon-48px.png", + "128": "images/gamepad-icon-128px.png" + }, + "default_title": "Open Gamepad Navigator settings." }, - "commands": { - "_execute_action": { - "suggested_key": { - "default": "Alt+Shift+G" - } - } + "options_ui": { + "open_in_tab": true, + "page": "html/settings.html" } } diff --git a/templates/LICENSE-banner.txt b/templates/LICENSE-banner.txt index 292cb62..474bb93 100644 --- a/templates/LICENSE-banner.txt +++ b/templates/LICENSE-banner.txt @@ -1,4 +1,4 @@ -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/html/text-input.html b/tests/html/text-input.html new file mode 100644 index 0000000..2706223 --- /dev/null +++ b/tests/html/text-input.html @@ -0,0 +1,41 @@ + + + + Text Input + + +

A sample page to try the new onscreen keyboard with various text inputs.

+ +

Input with No Type

+ + + +

Text Input

+ + + +

Search Input

+ + + +

Email Input

+ + + +

Password Input

+ + + +

Telephone Number Input

+ + + +

URL input

+ + + +

Text Area

+ + + + diff --git a/tests/js/arrows/arrows-mock-utils.js b/tests/js/arrows/arrows-mock-utils.js index ab5d925..aae7e4d 100644 --- a/tests/js/arrows/arrows-mock-utils.js +++ b/tests/js/arrows/arrows-mock-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/arrows/arrows-tests.js b/tests/js/arrows/arrows-tests.js index 78bb395..ac8359a 100644 --- a/tests/js/arrows/arrows-tests.js +++ b/tests/js/arrows/arrows-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/click/click-basic-tests.js b/tests/js/click/click-basic-tests.js index ddfbabc..66c3149 100644 --- a/tests/js/click/click-basic-tests.js +++ b/tests/js/click/click-basic-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -15,6 +15,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE (function (fluid, $) { "use strict"; + /** + * TODO: Add tests for links and other elements that involve navigation + * between pages. + */ + $(document).ready(function () { fluid.registerNamespace("gamepad"); diff --git a/tests/js/click/click-dropdown-tests.js b/tests/js/click/click-dropdown-tests.js index 6fbb0ef..2680a76 100644 --- a/tests/js/click/click-dropdown-tests.js +++ b/tests/js/click/click-dropdown-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/click/click-mock-utils.js b/tests/js/click/click-mock-utils.js index 0b3629a..cfa6267 100644 --- a/tests/js/click/click-mock-utils.js +++ b/tests/js/click/click-mock-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/gamepad-mock.js b/tests/js/gamepad-mock.js index bb08027..4977003 100644 --- a/tests/js/gamepad-mock.js +++ b/tests/js/gamepad-mock.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/scroll/scroll-bidirectional-diagonal-tests.js b/tests/js/scroll/scroll-bidirectional-diagonal-tests.js index a3ab184..f5331cb 100644 --- a/tests/js/scroll/scroll-bidirectional-diagonal-tests.js +++ b/tests/js/scroll/scroll-bidirectional-diagonal-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js b/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js index bc10605..c1f931f 100644 --- a/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js +++ b/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/scroll/scroll-mock-utils.js b/tests/js/scroll/scroll-mock-utils.js index 51ed2be..05503ba 100644 --- a/tests/js/scroll/scroll-mock-utils.js +++ b/tests/js/scroll/scroll-mock-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/scroll/scroll-unidirectional-axes-tests.js b/tests/js/scroll/scroll-unidirectional-axes-tests.js index d139d88..a816eb0 100644 --- a/tests/js/scroll/scroll-unidirectional-axes-tests.js +++ b/tests/js/scroll/scroll-unidirectional-axes-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/scroll/scroll-unidirectional-button-tests.js b/tests/js/scroll/scroll-unidirectional-button-tests.js index eaf9582..1152495 100644 --- a/tests/js/scroll/scroll-unidirectional-button-tests.js +++ b/tests/js/scroll/scroll-unidirectional-button-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/tab/tab-axes-tests.js b/tests/js/tab/tab-axes-tests.js index f71abf6..b7a9603 100644 --- a/tests/js/tab/tab-axes-tests.js +++ b/tests/js/tab/tab-axes-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/tab/tab-button-continuous-tests.js b/tests/js/tab/tab-button-continuous-tests.js index dba516a..2686c4d 100644 --- a/tests/js/tab/tab-button-continuous-tests.js +++ b/tests/js/tab/tab-button-continuous-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/tab/tab-button-discrete-tests.js b/tests/js/tab/tab-button-discrete-tests.js index bbad86d..511a6d3 100644 --- a/tests/js/tab/tab-button-discrete-tests.js +++ b/tests/js/tab/tab-button-discrete-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/tab/tab-mock-utils.js b/tests/js/tab/tab-mock-utils.js index 28849a7..371732d 100644 --- a/tests/js/tab/tab-mock-utils.js +++ b/tests/js/tab/tab-mock-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/tests/js/tab/tab-order-tests.js b/tests/js/tab/tab-order-tests.js index 59b6167..6f15449 100644 --- a/tests/js/tab/tab-order-tests.js +++ b/tests/js/tab/tab-order-tests.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. @@ -21,11 +21,12 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE jqUnit.module("Gamepad Navigator Tab Navigation DOM Tests", { setup: function () { + var testElementContainer = $(".test-elements"); /** * Get the list of tabbable elements in the order of their tabindex for * verification. */ - gamepad.tests.sortedTabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(gamepad.inputMapper.base.tabindexSortFilter); + gamepad.tests.tabbableElements = ally.query.tabsequence({ context: testElementContainer, strategy: "strict" }); } }); @@ -33,7 +34,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE jqUnit.expect(1); // Get the first element from the tabbable elements list. - var firstTabbableElement = gamepad.tests.sortedTabbableElements[0], + var firstTabbableElement = gamepad.tests.tabbableElements[0], hasTabindexOne = firstTabbableElement.getAttribute("tabindex") === "1"; jqUnit.assertTrue("The first element should have tabindex=\"1\"", hasTabindexOne); @@ -43,7 +44,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE jqUnit.expect(1); // Get the last element from the tabbable elements list. - var lastTabbableElement = gamepad.tests.sortedTabbableElements[gamepad.tests.sortedTabbableElements.length - 1], + var lastTabbableElement = gamepad.tests.tabbableElements[gamepad.tests.tabbableElements.length - 1], isLastElement = lastTabbableElement.getAttribute("id") === "last"; jqUnit.assertTrue("The first element should be the last div with id=\"last\"", isLastElement); @@ -54,12 +55,12 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // Check if the tabbable elements list contains elements with tabindex="-1". var elementWithNegativeTabindex = $("button[tabindex=\"-1\"]"), - hasElementWithNegativeTabindex = gamepad.tests.sortedTabbableElements.includes(elementWithNegativeTabindex[0]); + hasElementWithNegativeTabindex = gamepad.tests.tabbableElements.includes(elementWithNegativeTabindex[0]); jqUnit.assertFalse("The sorted elements array should not contain elements with tabindex=\"-1\"", hasElementWithNegativeTabindex); // Check if the tabbable elements list contains elements with tabindex="0". var elementWithTabindexZero = $("div[tabindex=\"0\"]"), - hasElementWithTabindexZero = gamepad.tests.sortedTabbableElements.includes(elementWithTabindexZero[0]); + hasElementWithTabindexZero = gamepad.tests.tabbableElements.includes(elementWithTabindexZero[0]); jqUnit.assertTrue("The sorted elements array should contain elements with tabindex=\"0\"", hasElementWithTabindexZero); }); }); diff --git a/tests/js/test-utils.js b/tests/js/test-utils.js index c60940c..96d633a 100644 --- a/tests/js/test-utils.js +++ b/tests/js/test-utils.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2020 The Gamepad Navigator Authors +Copyright (c) 2023 The Gamepad Navigator Authors See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. diff --git a/utils/bundleCss.js b/utils/bundleCss.js new file mode 100644 index 0000000..a24566f --- /dev/null +++ b/utils/bundleCss.js @@ -0,0 +1,56 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var fs = require("fs"); +var path = require("path"); + +var gamepad = fluid.registerNamespace("gamepad"); + +fluid.defaults("gamepad.bundleCss", { + gradeNames: ["fluid.component"], + outputPath: "dist/js/content_scripts/styles.js", + outputTemplate: "\"use strict\";\nvar gamepad = fluid.registerNamespace(\"gamepad\");\ngamepad.css = `%payload`;\n", + pathsToBundle: { + src: "src/css", + osk: "node_modules/fluid-osk/src/css" + }, + excludes: [ + "src/css/common.css", + "src/css/configuration-panel.css" + ], + listeners: { + "onCreate.processDirs": { + funcName: "gamepad.bundleCss.processDirs", + args: ["{that}"] + } + } +}); + +gamepad.bundleCss.processDirs = function (that) { + var payload = ""; + + var resolvedExcludes = that.options.excludes.map(function (relativePath) { + return path.resolve(relativePath); + }); + + fluid.each(that.options.pathsToBundle, function (pathToBundle) { + var filenames = fs.readdirSync(pathToBundle); + fluid.each(filenames, function (filename) { + if (filename.toLowerCase().endsWith(".css")) { + var filePath = path.resolve(pathToBundle, filename); + if (!resolvedExcludes.includes(filePath)) { + var fileContents = fs.readFileSync(filePath, { encoding: "utf8"}); + payload += fileContents; + } + } + }); + }); + + var bundle = fluid.stringTemplate(that.options.outputTemplate, { payload: payload}); + + fs.writeFileSync(that.options.outputPath, bundle, { encoding: "utf8"}); + + fluid.log(fluid.logLevel.WARN, "bundled all CSS to '" + that.options.outputPath + "'"); +}; + +gamepad.bundleCss(); diff --git a/utils/bundleSvgs.js b/utils/bundleSvgs.js new file mode 100644 index 0000000..abbb547 --- /dev/null +++ b/utils/bundleSvgs.js @@ -0,0 +1,55 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var fs = require("fs"); +var path = require("path"); + +var gamepad = fluid.registerNamespace("gamepad"); + +fluid.defaults("gamepad.bundleSvgs", { + gradeNames: ["fluid.component"], + outputPath: "dist/js/content_scripts/svgs.js", + outputTemplate: "\"use strict\";\nvar gamepad = fluid.registerNamespace(\"gamepad\");\nfluid.registerNamespace(\"gamepad.svg\");\n%payload\n", + singleFileTemplate: "\ngamepad.svg[\"%filename\"] = `%svgText`;\n", + pathsToBundle: { + src: "src/images" + }, + excludes: [], + listeners: { + "onCreate.processDirs": { + funcName: "gamepad.bundleSvgs.processDirs", + args: ["{that}"] + } + } +}); + +gamepad.bundleSvgs.processDirs = function (that) { + var payload = ""; + + var resolvedExcludes = that.options.excludes.map(function (relativePath) { + return path.resolve(relativePath); + }); + + fluid.each(that.options.pathsToBundle, function (pathToBundle) { + var filenames = fs.readdirSync(pathToBundle); + fluid.each(filenames, function (filename) { + if (filename.toLowerCase().endsWith(".svg")) { + var filenameMinusExtension = path.basename(filename, ".svg"); + var filePath = path.resolve(pathToBundle, filename); + if (!resolvedExcludes.includes(filePath)) { + var fileContents = fs.readFileSync(filePath, { encoding: "utf8"}); + var filePayload = fluid.stringTemplate(that.options.singleFileTemplate, { filename: filenameMinusExtension, svgText: fileContents}); + payload += filePayload; + } + } + }); + }); + + var bundle = fluid.stringTemplate(that.options.outputTemplate, { payload: payload}); + + fs.writeFileSync(that.options.outputPath, bundle, { encoding: "utf8"}); + + fluid.log(fluid.logLevel.WARN, "bundled all SVG files to '" + that.options.outputPath + "'"); +}; + +gamepad.bundleSvgs();