From 40cae008d807818861742ffee0b5c851db9770bf Mon Sep 17 00:00:00 2001 From: sguenter Date: Fri, 29 Sep 2023 17:45:29 +0200 Subject: [PATCH] initial commit --- .eslintrc.json | 191 ++++++++++++++++++++++++++++++++++++ .gitignore | 10 ++ .npnignore | 2 + .prettierrc | 5 + bin/build.js | 55 +++++++++++ bin/release.sh | 39 ++++++++ bin/updateCopies.js | 101 +++++++++++++++++++ package.json | 32 ++++++ src/AffectedEnum.ts | 5 + src/HotkeyDefinitionType.ts | 8 ++ src/HotkeyListener.ts | 9 ++ src/HotkeyManager.ts | 171 ++++++++++++++++++++++++++++++++ tsconfig.json | 39 ++++++++ 13 files changed, 667 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .npnignore create mode 100644 .prettierrc create mode 100644 bin/build.js create mode 100755 bin/release.sh create mode 100644 bin/updateCopies.js create mode 100644 package.json create mode 100644 src/AffectedEnum.ts create mode 100644 src/HotkeyDefinitionType.ts create mode 100644 src/HotkeyListener.ts create mode 100644 src/HotkeyManager.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..47851d7 --- /dev/null +++ b/.eslintrc.json @@ -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": { + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5af4966 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ + +/node_modules/* +node_modules/* + +#idea-ide +/.idea/* + +/src/hotkeys.ts + +dist diff --git a/.npnignore b/.npnignore new file mode 100644 index 0000000..aad6693 --- /dev/null +++ b/.npnignore @@ -0,0 +1,2 @@ +/bin/ +boostrapReactMobile.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ca0c14e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "singleQuote": true +} diff --git a/bin/build.js b/bin/build.js new file mode 100644 index 0000000..12a95a2 --- /dev/null +++ b/bin/build.js @@ -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"); diff --git a/bin/release.sh b/bin/release.sh new file mode 100755 index 0000000..e05415f --- /dev/null +++ b/bin/release.sh @@ -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" + diff --git a/bin/updateCopies.js b/bin/updateCopies.js new file mode 100644 index 0000000..7be6dcd --- /dev/null +++ b/bin/updateCopies.js @@ -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); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..84be4b3 --- /dev/null +++ b/package.json @@ -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": { + } +} diff --git a/src/AffectedEnum.ts b/src/AffectedEnum.ts new file mode 100644 index 0000000..9b0eca8 --- /dev/null +++ b/src/AffectedEnum.ts @@ -0,0 +1,5 @@ +export enum AffectedEnum { + NOT_FULL_FILLED = 0, + FULL_FILLED = 1, + FULL_FILLED_AND_CHANGED, +} diff --git a/src/HotkeyDefinitionType.ts b/src/HotkeyDefinitionType.ts new file mode 100644 index 0000000..ece5d64 --- /dev/null +++ b/src/HotkeyDefinitionType.ts @@ -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>; diff --git a/src/HotkeyListener.ts b/src/HotkeyListener.ts new file mode 100644 index 0000000..6e0a409 --- /dev/null +++ b/src/HotkeyListener.ts @@ -0,0 +1,9 @@ +import { AffectedEnum } from './AffectedEnum'; + +export type HotkeyListenerEvent = { + event: KeyboardEvent; + type: 'keydown' | 'keyup'; + subKeys: Record; +}; + +export type HotkeyListener = (ev: HotkeyListenerEvent) => unknown; diff --git a/src/HotkeyManager.ts b/src/HotkeyManager.ts new file mode 100644 index 0000000..aa0cc4b --- /dev/null +++ b/src/HotkeyManager.ts @@ -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 = { + keys: HotkeyDefinitionType[]; + subKeys: { [key in SubKeys]: HotkeyDefinitionType[] }; + callbacks: HotkeyListener[]; +}; + +export class HotkeyManager>> { + private keyPressedMap = new Map(); + 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: HotkeyName, + callback: HotkeyListener, + ) { + 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( + hotkey: HotkeyName, + subkey: keyof HotkeyConfig[HotkeyName]['subKeys'], + definition: HotkeyDefinitionType, + ) { + this.hotkeys[hotkey].subKeys[subkey as string].push(definition); + } + + removeSubKeyDefinition( + 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( + 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, + ); + + 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 })); + } + } + }); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..faabfb9 --- /dev/null +++ b/tsconfig.json @@ -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", + ] +}