Modifier key platform names (#519)
* wip * add environment class * use Environment class * use Environment for scanning modifier options * remove Environment in favor of API * await promise * use modifier symbols on macOS * fix key separator issues * if else to switch * simplify variable names
This commit is contained in:
parent
48cf646973
commit
d6a3825a38
@ -21,6 +21,7 @@
|
||||
|
||||
<script src="/mixed/js/core.js"></script>
|
||||
<script src="/mixed/js/dom.js"></script>
|
||||
<script src="/mixed/js/environment.js"></script>
|
||||
<script src="/mixed/js/japanese.js"></script>
|
||||
|
||||
<script src="/bg/js/anki.js"></script>
|
||||
|
@ -24,6 +24,7 @@
|
||||
* ClipboardMonitor
|
||||
* Database
|
||||
* DictionaryImporter
|
||||
* Environment
|
||||
* JsonSchema
|
||||
* Mecab
|
||||
* ObjectPropertyAccessor
|
||||
@ -35,6 +36,7 @@
|
||||
* optionsLoad
|
||||
* optionsSave
|
||||
* profileConditionsDescriptor
|
||||
* profileConditionsDescriptorPromise
|
||||
* requestJson
|
||||
* requestText
|
||||
* utilIsolate
|
||||
@ -42,6 +44,7 @@
|
||||
|
||||
class Backend {
|
||||
constructor() {
|
||||
this.environment = new Environment();
|
||||
this.database = new Database();
|
||||
this.dictionaryImporter = new DictionaryImporter();
|
||||
this.translator = new Translator(this.database);
|
||||
@ -100,7 +103,7 @@ class Backend {
|
||||
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
|
||||
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
|
||||
['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}],
|
||||
['getEnvironmentInfo', {async: true, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
|
||||
['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
|
||||
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
|
||||
['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}],
|
||||
['getQueryParserTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetQueryParserTemplatesHtml.bind(this)}],
|
||||
@ -140,9 +143,12 @@ class Backend {
|
||||
}, 1000);
|
||||
this._updateBadge();
|
||||
|
||||
await this.environment.prepare();
|
||||
await this.database.prepare();
|
||||
await this.translator.prepare();
|
||||
|
||||
await profileConditionsDescriptorPromise;
|
||||
|
||||
this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
|
||||
this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET');
|
||||
this.options = await optionsLoad();
|
||||
@ -635,15 +641,8 @@ class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
async _onApiGetEnvironmentInfo() {
|
||||
const browser = await Backend._getBrowser();
|
||||
const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
|
||||
return {
|
||||
browser,
|
||||
platform: {
|
||||
os: platform.os
|
||||
}
|
||||
};
|
||||
_onApiGetEnvironmentInfo() {
|
||||
return this.environment.getInfo();
|
||||
}
|
||||
|
||||
async _onApiClipboardGet() {
|
||||
@ -659,7 +658,7 @@ class Backend {
|
||||
being an extension with clipboard permissions. It effectively asks for the
|
||||
non-extension permission for clipboard access.
|
||||
*/
|
||||
const browser = await Backend._getBrowser();
|
||||
const {browser} = this.environment.getInfo();
|
||||
if (browser === 'firefox' || browser === 'firefox-mobile') {
|
||||
return await navigator.clipboard.readText();
|
||||
} else {
|
||||
@ -1211,23 +1210,4 @@ class Backend {
|
||||
// Edge throws exception for no reason here.
|
||||
}
|
||||
}
|
||||
|
||||
static async _getBrowser() {
|
||||
if (EXTENSION_IS_BROWSER_EDGE) {
|
||||
return 'edge';
|
||||
}
|
||||
if (typeof browser !== 'undefined') {
|
||||
try {
|
||||
const info = await browser.runtime.getBrowserInfo();
|
||||
if (info.name === 'Fennec') {
|
||||
return 'firefox-mobile';
|
||||
}
|
||||
} catch (e) {
|
||||
// NOP
|
||||
}
|
||||
return 'firefox';
|
||||
} else {
|
||||
return 'chrome';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* Environment
|
||||
*/
|
||||
|
||||
function _profileConditionTestDomain(urlDomain, domain) {
|
||||
return (
|
||||
@ -36,135 +39,140 @@ function _profileConditionTestDomainList(url, domainList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const _profileModifierKeys = [
|
||||
{optionValue: 'alt', name: 'Alt'},
|
||||
{optionValue: 'ctrl', name: 'Ctrl'},
|
||||
{optionValue: 'shift', name: 'Shift'}
|
||||
];
|
||||
let profileConditionsDescriptor = null;
|
||||
|
||||
if (!hasOwn(window, 'netscape')) {
|
||||
_profileModifierKeys.push({optionValue: 'meta', name: 'Meta'});
|
||||
}
|
||||
const profileConditionsDescriptorPromise = (async () => {
|
||||
const environment = new Environment();
|
||||
await environment.prepare();
|
||||
|
||||
const _profileModifierValueToName = new Map(
|
||||
_profileModifierKeys.map(({optionValue, name}) => [optionValue, name])
|
||||
);
|
||||
const modifiers = environment.getInfo().modifiers;
|
||||
const modifierSeparator = modifiers.separator;
|
||||
const modifierKeyValues = modifiers.keys.map(
|
||||
({value, name}) => ({optionValue: value, name})
|
||||
);
|
||||
|
||||
const _profileModifierNameToValue = new Map(
|
||||
_profileModifierKeys.map(({optionValue, name}) => [name, optionValue])
|
||||
);
|
||||
const modifierValueToName = new Map(
|
||||
modifierKeyValues.map(({optionValue, name}) => [optionValue, name])
|
||||
);
|
||||
|
||||
const profileConditionsDescriptor = {
|
||||
popupLevel: {
|
||||
name: 'Popup Level',
|
||||
description: 'Use profile depending on the level of the popup.',
|
||||
placeholder: 'Number',
|
||||
type: 'number',
|
||||
step: 1,
|
||||
defaultValue: 0,
|
||||
defaultOperator: 'equal',
|
||||
transform: (optionValue) => parseInt(optionValue, 10),
|
||||
transformReverse: (transformedOptionValue) => `${transformedOptionValue}`,
|
||||
validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue),
|
||||
operators: {
|
||||
equal: {
|
||||
name: '=',
|
||||
test: ({depth}, optionValue) => (depth === optionValue)
|
||||
},
|
||||
notEqual: {
|
||||
name: '\u2260',
|
||||
test: ({depth}, optionValue) => (depth !== optionValue)
|
||||
},
|
||||
lessThan: {
|
||||
name: '<',
|
||||
test: ({depth}, optionValue) => (depth < optionValue)
|
||||
},
|
||||
greaterThan: {
|
||||
name: '>',
|
||||
test: ({depth}, optionValue) => (depth > optionValue)
|
||||
},
|
||||
lessThanOrEqual: {
|
||||
name: '\u2264',
|
||||
test: ({depth}, optionValue) => (depth <= optionValue)
|
||||
},
|
||||
greaterThanOrEqual: {
|
||||
name: '\u2265',
|
||||
test: ({depth}, optionValue) => (depth >= optionValue)
|
||||
const modifierNameToValue = new Map(
|
||||
modifierKeyValues.map(({optionValue, name}) => [name, optionValue])
|
||||
);
|
||||
|
||||
profileConditionsDescriptor = {
|
||||
popupLevel: {
|
||||
name: 'Popup Level',
|
||||
description: 'Use profile depending on the level of the popup.',
|
||||
placeholder: 'Number',
|
||||
type: 'number',
|
||||
step: 1,
|
||||
defaultValue: 0,
|
||||
defaultOperator: 'equal',
|
||||
transform: (optionValue) => parseInt(optionValue, 10),
|
||||
transformReverse: (transformedOptionValue) => `${transformedOptionValue}`,
|
||||
validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue),
|
||||
operators: {
|
||||
equal: {
|
||||
name: '=',
|
||||
test: ({depth}, optionValue) => (depth === optionValue)
|
||||
},
|
||||
notEqual: {
|
||||
name: '\u2260',
|
||||
test: ({depth}, optionValue) => (depth !== optionValue)
|
||||
},
|
||||
lessThan: {
|
||||
name: '<',
|
||||
test: ({depth}, optionValue) => (depth < optionValue)
|
||||
},
|
||||
greaterThan: {
|
||||
name: '>',
|
||||
test: ({depth}, optionValue) => (depth > optionValue)
|
||||
},
|
||||
lessThanOrEqual: {
|
||||
name: '\u2264',
|
||||
test: ({depth}, optionValue) => (depth <= optionValue)
|
||||
},
|
||||
greaterThanOrEqual: {
|
||||
name: '\u2265',
|
||||
test: ({depth}, optionValue) => (depth >= optionValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
url: {
|
||||
name: 'URL',
|
||||
description: 'Use profile depending on the URL of the current website.',
|
||||
defaultOperator: 'matchDomain',
|
||||
operators: {
|
||||
matchDomain: {
|
||||
name: 'Matches Domain',
|
||||
placeholder: 'Comma separated list of domains',
|
||||
defaultValue: 'example.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),
|
||||
validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),
|
||||
test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue)
|
||||
},
|
||||
matchRegExp: {
|
||||
name: 'Matches RegExp',
|
||||
placeholder: 'Regular expression',
|
||||
defaultValue: 'example\\.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => new RegExp(optionValue, 'i'),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.source,
|
||||
test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
|
||||
}
|
||||
}
|
||||
},
|
||||
modifierKeys: {
|
||||
name: 'Modifier Keys',
|
||||
description: 'Use profile depending on the active modifier keys.',
|
||||
values: modifierKeyValues,
|
||||
defaultOperator: 'are',
|
||||
operators: {
|
||||
are: {
|
||||
name: 'are',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
keySeparator: modifierSeparator,
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(modifierSeparator)
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => modifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => modifierValueToName.get(v))
|
||||
.join(modifierSeparator),
|
||||
test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
areNot: {
|
||||
name: 'are not',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
keySeparator: modifierSeparator,
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(modifierSeparator)
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => modifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => modifierValueToName.get(v))
|
||||
.join(modifierSeparator),
|
||||
test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
include: {
|
||||
name: 'include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
|
||||
},
|
||||
notInclude: {
|
||||
name: 'don\'t include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
url: {
|
||||
name: 'URL',
|
||||
description: 'Use profile depending on the URL of the current website.',
|
||||
defaultOperator: 'matchDomain',
|
||||
operators: {
|
||||
matchDomain: {
|
||||
name: 'Matches Domain',
|
||||
placeholder: 'Comma separated list of domains',
|
||||
defaultValue: 'example.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),
|
||||
validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),
|
||||
test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue)
|
||||
},
|
||||
matchRegExp: {
|
||||
name: 'Matches RegExp',
|
||||
placeholder: 'Regular expression',
|
||||
defaultValue: 'example\\.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => new RegExp(optionValue, 'i'),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.source,
|
||||
test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
|
||||
}
|
||||
}
|
||||
},
|
||||
modifierKeys: {
|
||||
name: 'Modifier Keys',
|
||||
description: 'Use profile depending on the active modifier keys.',
|
||||
values: _profileModifierKeys,
|
||||
defaultOperator: 'are',
|
||||
operators: {
|
||||
are: {
|
||||
name: 'are',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(' + ')
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => _profileModifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => _profileModifierValueToName.get(v))
|
||||
.join(' + '),
|
||||
test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
areNot: {
|
||||
name: 'are not',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(' + ')
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => _profileModifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => _profileModifierValueToName.get(v))
|
||||
.join(' + '),
|
||||
test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
include: {
|
||||
name: 'include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
|
||||
},
|
||||
notInclude: {
|
||||
name: 'don\'t include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
@ -310,10 +310,14 @@ ConditionsUI.Condition = class Condition {
|
||||
inputInner.prop('readonly', true);
|
||||
|
||||
let values = [];
|
||||
let keySeparator = ' + ';
|
||||
for (const object of objects) {
|
||||
if (hasOwn(object, 'values')) {
|
||||
values = object.values;
|
||||
}
|
||||
if (hasOwn(object, 'keySeparator')) {
|
||||
keySeparator = object.keySeparator;
|
||||
}
|
||||
}
|
||||
|
||||
const pressedKeyIndices = new Set();
|
||||
@ -347,7 +351,7 @@ ConditionsUI.Condition = class Condition {
|
||||
}
|
||||
}
|
||||
|
||||
const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + ');
|
||||
const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(keySeparator);
|
||||
inputInner.val(inputValue);
|
||||
inputInner.change();
|
||||
};
|
||||
|
@ -22,6 +22,7 @@
|
||||
* ankiTemplatesInitialize
|
||||
* ankiTemplatesUpdateValue
|
||||
* apiForwardLogsToBackend
|
||||
* apiGetEnvironmentInfo
|
||||
* apiOptionsSave
|
||||
* appearanceInitialize
|
||||
* audioSettingsInitialize
|
||||
@ -285,6 +286,23 @@ function showExtensionInformation() {
|
||||
node.textContent = `${manifest.name} v${manifest.version}`;
|
||||
}
|
||||
|
||||
async function settingsPopulateModifierKeys() {
|
||||
const scanModifierKeySelect = document.querySelector('#scan-modifier-key');
|
||||
scanModifierKeySelect.textContent = '';
|
||||
|
||||
const environment = await apiGetEnvironmentInfo();
|
||||
const modifierKeys = [
|
||||
{value: 'none', name: 'None'},
|
||||
...environment.modifiers.keys
|
||||
];
|
||||
for (const {value, name} of modifierKeys) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = name;
|
||||
scanModifierKeySelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function onReady() {
|
||||
apiForwardLogsToBackend();
|
||||
@ -292,6 +310,7 @@ async function onReady() {
|
||||
|
||||
showExtensionInformation();
|
||||
|
||||
await settingsPopulateModifierKeys();
|
||||
formSetupEventListeners();
|
||||
appearanceInitialize();
|
||||
await audioSettingsInitialize();
|
||||
|
@ -23,6 +23,7 @@
|
||||
* getOptionsFullMutable
|
||||
* getOptionsMutable
|
||||
* profileConditionsDescriptor
|
||||
* profileConditionsDescriptorPromise
|
||||
* settingsSaveOptions
|
||||
* utilBackgroundIsolate
|
||||
*/
|
||||
@ -98,6 +99,7 @@ async function profileFormWrite(optionsFull) {
|
||||
profileConditionsContainer.cleanup();
|
||||
}
|
||||
|
||||
await profileConditionsDescriptorPromise;
|
||||
profileConditionsContainer = new ConditionsUI.Container(
|
||||
profileConditionsDescriptor,
|
||||
'popupLevel',
|
||||
@ -128,7 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi
|
||||
}
|
||||
|
||||
async function profileOptionsUpdateTarget(optionsFull) {
|
||||
profileFormWrite(optionsFull);
|
||||
await profileFormWrite(optionsFull);
|
||||
|
||||
const optionsContext = getOptionsContext();
|
||||
const options = await getOptionsMutable(optionsContext);
|
||||
|
@ -412,13 +412,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="scan-modifier-key">Scan modifier key</label>
|
||||
<select class="form-control" id="scan-modifier-key">
|
||||
<option value="none">None</option>
|
||||
<option value="alt">Alt</option>
|
||||
<option value="ctrl">Ctrl</option>
|
||||
<option value="shift">Shift</option>
|
||||
<option data-hide-for-browser="firefox firefox-mobile" value="meta">Meta</option>
|
||||
</select>
|
||||
<select class="form-control" id="scan-modifier-key"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1131,6 +1125,7 @@
|
||||
|
||||
<script src="/mixed/js/core.js"></script>
|
||||
<script src="/mixed/js/dom.js"></script>
|
||||
<script src="/mixed/js/environment.js"></script>
|
||||
<script src="/mixed/js/api.js"></script>
|
||||
<script src="/mixed/js/japanese.js"></script>
|
||||
|
||||
|
114
ext/mixed/js/environment.js
Normal file
114
ext/mixed/js/environment.js
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
class Environment {
|
||||
constructor() {
|
||||
this._cachedEnvironmentInfo = null;
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
this._cachedEnvironmentInfo = await this._loadEnvironmentInfo();
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
if (this._cachedEnvironmentInfo === null) { throw new Error('Not prepared'); }
|
||||
return this._cachedEnvironmentInfo;
|
||||
}
|
||||
|
||||
async _loadEnvironmentInfo() {
|
||||
const browser = await this._getBrowser();
|
||||
const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
|
||||
const modifierInfo = this._getModifierInfo(browser, platform.os);
|
||||
return {
|
||||
browser,
|
||||
platform: {
|
||||
os: platform.os
|
||||
},
|
||||
modifiers: modifierInfo
|
||||
};
|
||||
}
|
||||
|
||||
async _getBrowser() {
|
||||
if (EXTENSION_IS_BROWSER_EDGE) {
|
||||
return 'edge';
|
||||
}
|
||||
if (typeof browser !== 'undefined') {
|
||||
try {
|
||||
const info = await browser.runtime.getBrowserInfo();
|
||||
if (info.name === 'Fennec') {
|
||||
return 'firefox-mobile';
|
||||
}
|
||||
} catch (e) {
|
||||
// NOP
|
||||
}
|
||||
return 'firefox';
|
||||
} else {
|
||||
return 'chrome';
|
||||
}
|
||||
}
|
||||
|
||||
_getModifierInfo(browser, os) {
|
||||
let osKeys;
|
||||
let separator;
|
||||
switch (os) {
|
||||
case 'win':
|
||||
separator = ' + ';
|
||||
osKeys = [
|
||||
['alt', 'Alt'],
|
||||
['ctrl', 'Ctrl'],
|
||||
['shift', 'Shift'],
|
||||
['meta', 'Windows']
|
||||
];
|
||||
break;
|
||||
case 'mac':
|
||||
separator = '';
|
||||
osKeys = [
|
||||
['alt', '⌥'],
|
||||
['ctrl', '⌃'],
|
||||
['shift', '⇧'],
|
||||
['meta', '⌘']
|
||||
];
|
||||
break;
|
||||
case 'linux':
|
||||
case 'openbsd':
|
||||
case 'cros':
|
||||
case 'android':
|
||||
separator = ' + ';
|
||||
osKeys = [
|
||||
['alt', 'Alt'],
|
||||
['ctrl', 'Ctrl'],
|
||||
['shift', 'Shift'],
|
||||
['meta', 'Super']
|
||||
];
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid OS: ${os}`);
|
||||
}
|
||||
|
||||
const isFirefox = (browser === 'firefox' || browser === 'firefox-mobile');
|
||||
const keys = [];
|
||||
|
||||
for (const [value, name] of osKeys) {
|
||||
// Firefox doesn't support event.metaKey on platforms other than macOS
|
||||
if (value === 'meta' && isFirefox && os !== 'mac') { continue; }
|
||||
keys.push({value, name});
|
||||
}
|
||||
|
||||
return {keys, separator};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user