initial commit

This commit is contained in:
sguenter 2023-09-29 17:45:29 +02:00
commit 40cae008d8
13 changed files with 667 additions and 0 deletions

191
.eslintrc.json Normal file
View File

@ -0,0 +1,191 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"airbnb",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/prettier",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"react",
"@typescript-eslint",
"react-hooks"
],
"rules": {
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"react/jsx-uses-react": [
"error"
],
"react/jsx-uses-vars": [
"error"
],
"react-hooks/rules-of-hooks": "error",
// Checks rules of Hooks
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": ""
}
],
// Checks effect dependencies
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".tsx"
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": [
"error"
],
"lines-between-class-members": [
"warn",
"always",
{
"exceptAfterSingleLine": true
}
],
"react/sort-comp": [
"warn",
{
"order": [
"static-variables",
"instance-variables",
"static-methods",
"lifecycle",
"render",
"/^render.+$/",
"instance-methods",
"everything-else"
]
}
],
"no-return-assign": [
"error",
"except-parens"
],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*",
"src/pages/!(api)/**/*.tsx",
"src/pages/**/*.tsx",
"src/!(pages|models|app)/**/*.(tsx|ts)"
]
}
],
"react/destructuring-assignment": [
"error",
"always",
{
"ignoreClassFields": true
}
],
"react/state-in-constructor": [
"error",
"never"
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"vars": "all",
"ignoreRestSiblings": false,
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"import/no-cycle": [
"error",
{
"maxDepth": 1
}
],
"no-promise-executor-return": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"no-console": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/order": "off",
"import/prefer-default-export": "off",
"react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-boolean-value": "off",
"no-plusplus": "off",
"no-param-reassign": "off",
"default-case": "off",
"jsx-a11y/interactive-supports-focus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/label-has-associated-control": "off",
"react/require-default-props": "off"
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"typescript": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
},
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
],
"moduleDirectory": [
"node_modules",
"src/"
]
}
}
},
"globals": {
}
}

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/node_modules/*
node_modules/*
#idea-ide
/.idea/*
/src/hotkeys.ts
dist

2
.npnignore Normal file
View File

@ -0,0 +1,2 @@
/bin/
boostrapReactMobile.ts

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true
}

55
bin/build.js Normal file
View File

@ -0,0 +1,55 @@
const path = require("path");
const fs = require('fs');
const tmpFile = "./tmp/script.js";
function findNames(dir, excluded) {
let names = {};
if (excluded.includes(dir)) {
return names;
}
let files = fs.readdirSync(dir);
files.forEach(file => {
let stats = fs.statSync(dir + file);
if (stats.isDirectory()) {
let nameObject = findNames(dir + file + '/', excluded);
names = Object.assign(names, nameObject);
} else if ((file.endsWith(".ts") ) && !excluded.includes(dir + file)) {
names[file.substring(0, file.length - 3)] = dir + file.substring(0, file.length - 3);
}
else if ((file.endsWith(".mjs") ) && !excluded.includes(dir + file)) {
names[file.substring(0, file.length - 4)] = dir + file.substring(0, file.length - 4);
}
});
return names;
}
async function buildEntryPoints(fileOption, target) {
const cutLengthFront = 0;
target = target || tmpFile;
const resultDir = path.resolve(process.cwd(), path.dirname(target));
let names = {};
fileOption.input.forEach(dir => {
Object.assign(names, findNames(dir + "/", []));
});
let imports = '';
for (let k in names) {
imports += "export * from './" + path.relative(resultDir, path.resolve(process.cwd(), names[k].substring(cutLengthFront))) + "';\n";
}
if (!fs.existsSync(resultDir)) {
fs.mkdirSync(resultDir);
}
fs.writeFileSync(target, imports);
}
buildEntryPoints({
input: [
path.resolve(process.cwd(), "src/"),
],
}, "./src/hotkeys.ts");

39
bin/release.sh Executable file
View File

@ -0,0 +1,39 @@
#! /bin/bash
# Exit when a command fails
set -e
REPOSITORY=git@github.com:Ainias/js-helper.git
if [[ -z "$1" ]]; then
echo "versioname not given!"
exit;
fi;
versionName=$1
versionExists="$(git ls-remote $REPOSITORY refs/tags/"$versionName"| tr -d '\n')"
if [ -n "$versionExists" ]; then
echo "Version existiert bereits!";
exit 1;
fi;
WORKING_DIR=$(pwd)
TMPDIR=$(mktemp -d)
cd "$TMPDIR";
git clone $REPOSITORY project
cd project
npm install
npm run build
git add -u
git commit -m "pre-version-commit for version $versionName" || echo "no commit needed"
npm version "$versionName"
npm publish
git push
cd "$WORKING_DIR"
git pull;
echo "$TMPDIR"

101
bin/updateCopies.js Normal file
View File

@ -0,0 +1,101 @@
const path = require("path");
const exec = require('child_process').exec;
const fs = require('fs');
const packageName = require("../package.json").name;
let pathsToProjects = [
"/Users/sguenter/Projekte/Privat/dnd",
// "/home/silas/Projekte/web/nextjsTest/poc-nextjs",
// "/home/silas/Projekte/web/project-echo",
// "/home/silas/Projekte/web/smd-mail",
// "/home/silas/Projekte/web/dnd",
// "/home/silas/Projekte/web/bat",
// "/home/silas/Projekte/web/typeorm-sync",
// "/home/silas/Projekte/web/typeorm-sync-nextjs",
// "/home/silas/Projekte/web/worktime",
// "/home/silas/Projekte/web/TaskList",
// "/home/silas/Projekte/web/hoffnungsfest",
// "/home/silas/Projekte/web/geometry",
// "/home/silas/Projekte/web/react-bootstrap-mobile",
// "/home/silas/Projekte/web/react-bootstrap-mobile",
// "/home/silas/Projekte/chrome/dmscreen",
// "/home/silas/Projekte/web/smd-mail",
// "/home/silas/Projekte/web/prayercircle",
// "/home/silas/Projekte/Web/stories",
// "/home/silas/Projekte/web/cordova-sites",
// "/home/silas/Projekte/web/cordova-sites-easy-sync",
// "/home/silas/Projekte/Web/cordova-sites-user-management",
// "/home/silas/Projekte/i9/mbb",
// "/home/silas/Projekte/Web/bible-lexicon",
// "/var/www/i9/mbb",
// "/home/silas/PhpstormProjects/cordova-sites-user-management",
// "/home/silas/PhpstormProjects/project-echo",
];
const deleteFolderRecursive = function(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function(file, index){
let curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
async function execPromise(command) {
return new Promise((resolve, reject) => {
console.log("executing " + command + "...");
exec(command, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
if (err) {
reject([err, stdout, stderr]);
} else {
resolve([stdout, stderr]);
}
});
});
}
execPromise("npm pack").then(async (std) => {
let thisPath = process.cwd();
let name = std[0].trim();
let pathToTar = path.resolve(thisPath, name);
if (!fs.existsSync("tmp")) {
fs.mkdirSync("tmp");
}
process.chdir("tmp");
await execPromise("tar -xvzf " + pathToTar + " -C ./");
process.chdir("package");
fs.unlinkSync("package.json");
let promise = Promise.resolve();
pathsToProjects.forEach((project) => {
promise = promise.then(async () => {
let resultDir = path.resolve(project, "node_modules", packageName);
console.log(resultDir, fs.existsSync(resultDir));
if (!fs.existsSync(resultDir)) {
fs.mkdirSync(resultDir);
}
return execPromise("cp -r ./* "+resultDir);
});
});
await promise;
process.chdir(thisPath);
fs.unlinkSync(name);
deleteFolderRecursive("tmp");
// fs.unlinkSync("tmp");
console.log("done!");
}).catch(e => {
console.error(e);
});

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "@ainias42/hotkeys",
"version": "0.0.1",
"description": "Hotkey manager",
"main": "dist/hotkeys",
"scripts": {
"build": "node bin/build.js & tsc",
"update packages": "npm run build && node bin/updateCopies.js"
},
"keywords": [],
"author": "Silas Günther",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^20.7.1",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"eslint": "^8.50.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"@ainias42/js-helper": "^0.8.9"
},
"dependencies": {
}
}

5
src/AffectedEnum.ts Normal file
View File

@ -0,0 +1,5 @@
export enum AffectedEnum {
NOT_FULL_FILLED = 0,
FULL_FILLED = 1,
FULL_FILLED_AND_CHANGED,
}

View File

@ -0,0 +1,8 @@
import { URecord } from '@ainias42/js-helper';
export const specialKeys = ['meta', 'control', 'alt', 'shift', 'tab'] as const;
export type HotkeyDefinitionType = {
keys: string[];
ignoreFormElements?: boolean;
} & URecord<(typeof specialKeys)[number], boolean>;

9
src/HotkeyListener.ts Normal file
View File

@ -0,0 +1,9 @@
import { AffectedEnum } from './AffectedEnum';
export type HotkeyListenerEvent<SubKeys extends string | symbol | number> = {
event: KeyboardEvent;
type: 'keydown' | 'keyup';
subKeys: Record<SubKeys, AffectedEnum>;
};
export type HotkeyListener<SubKeys extends string | symbol | number> = (ev: HotkeyListenerEvent<SubKeys>) => unknown;

171
src/HotkeyManager.ts Normal file
View File

@ -0,0 +1,171 @@
import { HotkeyDefinitionType, specialKeys } from './HotkeyDefinitionType';
import { HotkeyListener } from './HotkeyListener';
import { JsonHelper, ObjectHelper } from '@ainias42/js-helper';
import { AffectedEnum } from './AffectedEnum';
type HotkeyEntry<SubKeys extends string> = {
keys: HotkeyDefinitionType[];
subKeys: { [key in SubKeys]: HotkeyDefinitionType[] };
callbacks: HotkeyListener<SubKeys>[];
};
export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<string>>> {
private keyPressedMap = new Map<string, boolean>();
private hotkeys: HotkeyConfig;
private enabled = true;
constructor(hotkeys: HotkeyConfig) {
this.hotkeys = hotkeys;
this.addKeyListeners();
}
private static isFormElement(element: EventTarget | null) {
return (
element instanceof HTMLInputElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLTextAreaElement
);
}
private static isEqual(a: HotkeyDefinitionType, b: HotkeyDefinitionType) {
return JsonHelper.deepEqual(a, b);
}
addListener<HotkeyName extends keyof HotkeyConfig>(
hotkeyName: HotkeyName,
callback: HotkeyListener<keyof HotkeyConfig[HotkeyName]['subKeys']>,
) {
const { callbacks } = this.hotkeys[hotkeyName];
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
};
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
addHotKeyDefinition(hotkey: keyof HotkeyConfig, definition: HotkeyDefinitionType) {
this.hotkeys[hotkey].keys.push(definition);
}
removeHotkeyDefinition(hotkey: keyof HotkeyConfig, definition: HotkeyDefinitionType) {
this.hotkeys[hotkey].keys = this.hotkeys[hotkey].keys.filter((key) => !HotkeyManager.isEqual(key, definition));
}
setHotkeyDefinitions(hotkey: keyof HotkeyConfig, definitions: HotkeyDefinitionType[]) {
this.hotkeys[hotkey].keys = definitions;
}
addSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definition: HotkeyDefinitionType,
) {
this.hotkeys[hotkey].subKeys[subkey as string].push(definition);
}
removeSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definition: HotkeyDefinitionType,
) {
const { subKeys } = this.hotkeys[hotkey];
subKeys[subkey as string] = subKeys[subkey as string].filter((key) => !HotkeyManager.isEqual(key, definition));
}
setSubKeyDefinitions<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definitions: HotkeyDefinitionType[],
) {
this.hotkeys[hotkey].subKeys[subkey as string] = definitions;
}
getConfig() {
return this.hotkeys;
}
private addKeyListeners() {
window.addEventListener('keydown', (e) => {
this.keyPressedMap.set(e.key.toLowerCase(), true);
this.checkHotkeys(e, 'keydown');
});
window.addEventListener('keyup', (e) => {
// Check first as afterwards the keys are not set
this.checkHotkeys(e, 'keyup');
this.keyPressedMap.set(e.key.toLowerCase(), false);
if (e.key === 'Meta') {
this.keyPressedMap.clear();
}
});
}
private checkHotkeyDefinition(keyDefinition: HotkeyDefinitionType, event: KeyboardEvent): AffectedEnum {
if (!this.enabled) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (keyDefinition.ignoreFormElements !== false && HotkeyManager.isFormElement(event.target)) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (keyDefinition.keys.some((key) => this.keyPressedMap.get(key) !== true)) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (
specialKeys.some(
(key) => keyDefinition[key] !== undefined && this.keyPressedMap.get(key) !== keyDefinition[key],
)
) {
return AffectedEnum.NOT_FULL_FILLED;
}
// Check if key is inside keyDefinition
const changedKey = event.key.toLowerCase();
if (!keyDefinition.keys.includes(changedKey) && !(changedKey in keyDefinition)) {
return AffectedEnum.FULL_FILLED;
}
return AffectedEnum.FULL_FILLED_AND_CHANGED;
}
private getKeyReducer(event: KeyboardEvent) {
return (acc: AffectedEnum, key: HotkeyDefinitionType) => {
if (acc === AffectedEnum.FULL_FILLED_AND_CHANGED) {
return acc;
}
return Math.max(acc, this.checkHotkeyDefinition(key, event));
};
}
private checkHotkeys(event: KeyboardEvent, type: 'keydown' | 'keyup') {
ObjectHelper.values(this.hotkeys).forEach((hotkey) => {
const isAffected = hotkey.keys.reduce(this.getKeyReducer(event), AffectedEnum.NOT_FULL_FILLED);
if (isAffected !== AffectedEnum.NOT_FULL_FILLED) {
const subKeys = ObjectHelper.entries(hotkey.subKeys).reduce(
(acc, [key, value]) => {
acc[key] = value.reduce(this.getKeyReducer(event), AffectedEnum.NOT_FULL_FILLED);
return acc;
},
{} as Record<string, AffectedEnum>,
);
if (
isAffected === AffectedEnum.FULL_FILLED_AND_CHANGED ||
ObjectHelper.values(subKeys).some((value) => value === AffectedEnum.FULL_FILLED_AND_CHANGED)
) {
hotkey.callbacks.forEach((callback) => callback({ event, subKeys, type }));
}
}
});
}
}

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"baseUrl": "./src",
"declaration": true,
"isolatedModules": true,
"lib": [
"es6",
"es2016",
"es2017",
"es2021",
"dom"
],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedParameters": true,
"outDir": "./dist",
"resolveJsonModule": true,
"sourceMap": true,
"strict": false,
"strictFunctionTypes": false,
"strictNullChecks": true,
"target": "es6",
},
"include": [
"./src/hotkeys.ts",
"./src/**/*.ts"
],
"exclude": [
"dist",
"bin",
"node_modules",
]
}