diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06cf653 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cache diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..42e753f --- /dev/null +++ b/.npmignore @@ -0,0 +1,31 @@ +# Source files +src/ +*.ts +!*.d.ts + +# Development files +tsconfig.*.json +ng-package.json +karma.conf.js +*.spec.ts + +# Build files +node_modules/ +coverage/ +.nyc_output/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b72f532 --- /dev/null +++ b/LICENSE @@ -0,0 +1,55 @@ +Workshack Input Library License + +Copyright (c) 2025 Workshack Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to use, +copy, modify, merge, publish, and distribute the Software, subject to the +following conditions: + +NON-COMMERCIAL USE: +For non-commercial applications (applications that do not generate revenue, +do not contain paid features, subscriptions, advertisements, or any form of +monetization), the Software may be used freely without any additional +requirements. + +COMMERCIAL USE: +For commercial applications (applications that generate revenue through sales, +subscriptions, advertisements, in-app purchases, or any other form of +monetization), the following additional requirement applies: + +- The application MUST include attribution to the Workshack Input Library in a + publicly accessible location within the application. This can be: + * An "About" page or section + * A "Credits" or "Acknowledgments" page + * A "For Developers" page + * Any other location where users can reasonably find it + +The attribution must include: +- The name "Workshack Input Library" +- A link to the library's repository or npm package page +- The copyright notice "© 2025 Workshack Team" + +Example attribution: +"This application uses Workshack Input Library (https://www.npmjs.com/package/@workshack/input) © 2025 Workshack Team" + +GENERAL CONDITIONS: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +DEFINITIONS: +- "Commercial application" means any software application, website, or service + that generates revenue or is intended to generate revenue through any means + including but not limited to: sales, subscriptions, advertisements, in-app + purchases, premium features, or commercial licensing. +- "Non-commercial application" means any software application, website, or + service that is provided free of charge with no revenue generation or + monetization of any kind. diff --git a/README.md b/README.md index 187234e..66013a0 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,454 @@ -# @ngshack/input +# @workshack/input -Biblioteka komponentów input i nawigacji dla aplikacji Webland z obsługą gamepadów. +Workshack input components library with gamepad and keyboard navigation support for Angular applications. -## Instalacja +## Installation ```bash -npm install @ngshack/input +npm install @workshack/input ``` -## Development +## Services -### Prerequisites -- Node.js 18+ -- Angular CLI 17+ +### InputService -### Setup -```bash -npm install -``` - -### Generate Components -```bash -# Generate new component -ng generate component components/my-component --project=input - -# Generate service -ng generate service services/my-service --project=input - -# Generate module -ng generate module modules/my-module --project=input -``` - -### Build Library -```bash -ng build input -``` - -### Test -```bash -ng test input -``` - -### Publish -```bash -npm run build:libs -npm publish dist/input -``` - -## Usage - -### Navigation Module +**Description:** Core service for managing input devices, actions, and schemes. +**Usage:** ```typescript -import { NavigationModule } from '@ngshack/input'; +import { InputService } from '@workshack/input'; @Component({ - imports: [NavigationModule], - template: ` -
- -
- ` + // ... }) export class MyComponent { - onConfirm() { - console.log('Confirmed!'); + constructor(private inputService: InputService) { + // Define custom actions + const jumpAction = this.inputService.defineAction('jump'); + jumpAction.onDown(() => console.log('Jump pressed!')); + jumpAction.onUp(() => console.log('Jump released!')); + + // Create input scheme + const gameScheme = this.inputService.defineScheme('game'); + + // Listen for device changes + this.inputService.deviceListChanged.subscribe(devices => { + console.log('Available devices:', devices); + }); } } ``` -### Action Binding Directive +**Methods:** +- `defineAction(name: string): InputAction` - Creates a new input action +- `defineScheme(name: string): InputScheme` - Creates a new input scheme +- `getDevices(): InputDevice[]` - Returns list of available input devices +**Properties:** +- `actions: InputAction[]` - Array of defined actions +- `schemes: InputScheme[]` - Array of defined schemes +- `devices: InputDevice[]` - Array of connected devices +- `deviceListChanged: BehaviorSubject` - Observable for device changes + +--- + +### ControlService (Example Integration) + +**Description:** Higher-level service that integrates with InputService to provide common navigation actions. + +**Usage:** ```typescript -import { ActionBindingDirective } from '@ngshack/input'; +import { ControlService } from 'your-app/services/control.service'; @Component({ - imports: [ActionBindingDirective], - template: ` -
- Back Button -
- ` + // ... }) export class MyComponent { + constructor(private controlService: ControlService) { + // Service automatically sets up common actions: + // - Left, Right, Up, Down navigation + // - Accept, Back, Exit actions + // - Hint/Help action + + // Get key names for display + const keyNames = this.controlService.getKeyNames('Enter', 'Escape'); + console.log('Key names:', keyNames); // ['Enter', 'Escape'] or themed names + } +} +``` + +**Static Properties:** +- `keyLeft: InputAction` - Left navigation action +- `keyRight: InputAction` - Right navigation action +- `keyUp: InputAction` - Up navigation action +- `keyDown: InputAction` - Down navigation action +- `keyAccept: InputAction` - Accept/confirm action +- `keyBack: InputAction` - Back/cancel action +- `keyExit: InputAction` - Exit action +- `keyHint: InputAction` - Hint/help action + +**Methods:** +- `getKeyNames(...names): string[]` - Get themed key names for display +- `clearKeyBindings()` - Clear all key bindings from schemes +- `bindActionsToScheme(device: InputDevice)` - Bind actions to device scheme + +## Directives + +### ActionBindingDirective + +**Selector:** `[inputActionBinding]` +**Description:** Binds an action name to an HTML element. + +**Usage:** +```html + +
Back
+``` + +**Properties:** +- `inputActionBinding: string` - Action name to bind to the element + +**Features:** +- Automatically sets `action` attribute on the element +- Works with keyboard and gamepad navigation + +--- + +### KeyItemDirective + +**Selector:** `[inputKeyItem]` +**Description:** Makes an element navigable with keyboard/gamepad and handles focus states. + +**Usage:** +```html + +``` + +**Properties:** +- `active: string` - CSS class name for active/focused state (default: 'active') +- `actionBindings: ActionBindings` - Object mapping action names to callback functions + +**Events:** +- `activated: EventEmitter` - Emitted when item is activated +- `deactivated: EventEmitter` - Emitted when item is deactivated +- `focused: EventEmitter` - Emitted when item receives focus +- `blurred: EventEmitter` - Emitted when item loses focus + +**Static Methods:** +- `FireAction(action: string): boolean` - Fires action on currently focused item +- `ActivateCurrentItem()` - Activates the currently focused item +- `SelectNexItem(fromAngle: number, toAngle: number)` - Selects next item in direction + +--- + +### KeyItemGroupDirective + +**Selector:** `[inputKeyItemGroup]` +**Description:** Groups KeyItem elements for scoped navigation. + +**Usage:** +```html +
+ + + +
+``` + +**Features:** +- Navigation is scoped to items within the group +- Automatically manages focus within the group +- Supports nested groups + +## Models + +### InputAction + +**Description:** Represents a bindable input action (e.g., "jump", "fire", "menu"). + +**Usage:** +```typescript +const jumpAction = new InputAction('jump'); + +// Bind callbacks +jumpAction.onDown((value, data, angle, timePressed) => { + console.log('Jump started!', { value, data, angle, timePressed }); +}); + +jumpAction.onUp((value, data, angle, timePressed) => { + console.log('Jump ended!', { value, data, angle, timePressed }); +}); + +jumpAction.onChange((value, data, angle, timePressed) => { + console.log('Jump value changed:', value); +}); + +// Fire the action +jumpAction.fire(1.0); // Press +jumpAction.fire(0.0); // Release +``` + +**Methods:** +- `onDown(callback: ActionCallback)` - Register callback for action press +- `onUp(callback: ActionCallback)` - Register callback for action release +- `onChange(callback: ActionCallback)` - Register callback for value changes +- `fire(value: number, data?: unknown, angle?: number, timePressed?: number)` - Fire the action +- `mapKeysToScheme(scheme: InputScheme, keys: (string|number)[])` - Map keys to scheme + +--- + +### InputDevice + +**Description:** Base class for input devices (keyboard, gamepad). + +**Properties:** +- `name: string` - Device name +- `type: string` - Device type ('keyboard', 'gamepad') + +**Methods:** +- `hasScheme(): boolean` - Check if device has an input scheme +- `mapKey(action: InputAction, keys: (string|number)[])` - Map keys to action + +--- + +### InputKeyboard + +**Description:** Keyboard input device implementation. + +**Usage:** +```typescript +const keyboard = new InputKeyboard('Main Keyboard'); +keyboard.mapKey(jumpAction, ['Space', 'KeyW']); +``` + +--- + +### InputGamepad + +**Description:** Gamepad input device implementation with automatic detection. + +**Static Properties:** +- `DeviceConnected: Subject` - Observable for gamepad connections +- `DeviceDisconnected: Subject` - Observable for gamepad disconnections + +**Static Methods:** +- `Init()` - Initialize gamepad detection + +**Usage:** +```typescript +// Listen for gamepad events +InputGamepad.DeviceConnected.subscribe(gamepad => { + console.log('Gamepad connected:', gamepad.name); + gamepad.mapKey(jumpAction, [0]); // Map to button 0 +}); + +InputGamepad.DeviceDisconnected.subscribe(gamepad => { + console.log('Gamepad disconnected:', gamepad.name); +}); + +// Initialize detection +InputGamepad.Init(); +``` + +## Complete Example + +```typescript +import { Component, OnInit } from '@angular/core'; +import { + InputService, + NavigationModule, + KeyItemDirective, + KeyItemGroupDirective, + ActionBindingDirective +} from '@workshack/input'; + +@Component({ + selector: 'app-game-menu', + imports: [ + NavigationModule, + KeyItemDirective, + KeyItemGroupDirective, + ActionBindingDirective + ], + template: ` +
+

Game Menu

+ + + + + + + +
+ Use arrow keys or gamepad to navigate + Press Enter or A to select +
+
+ `, + styles: [` + .game-menu { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 2rem; + } + + .menu-item { + padding: 1rem 2rem; + background: #333; + color: white; + border: 2px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + } + + .menu-item.active, + .menu-item:hover { + background: #555; + border-color: #007bff; + transform: scale(1.05); + } + + .controls-hint { + display: flex; + flex-direction: column; + text-align: center; + font-size: 0.8rem; + color: #666; + margin-top: 2rem; + } + `] +}) +export class GameMenuComponent implements OnInit { + + constructor(private inputService: InputService) {} + + ngOnInit() { + // Set up custom actions + const menuAction = this.inputService.defineAction('menu'); + menuAction.onUp(() => this.toggleMenu()); + + // Listen for device changes + this.inputService.deviceListChanged.subscribe(devices => { + console.log('Input devices:', devices.map(d => d.name)); + }); + } + + startGame() { + console.log('Starting game...'); + } + + showOptions() { + console.log('Showing options...'); + } + + exitGame() { + console.log('Exiting game...'); + } + goBack() { - // Handle back action + console.log('Going back...'); + return false; // Prevent default navigation + } + + playHoverSound() { + // Play hover sound effect + console.log('Hover sound'); + } + + toggleMenu() { + console.log('Toggle menu'); } } ``` ## Features -- 🎮 Gamepad support -- ⌨️ Keyboard navigation -- 🖱️ Mouse/touch support -- 🎯 Action binding system -- 📱 Responsive design -- 🎨 Gaming console theme +- 🎮 **Gamepad Support** - Automatic detection and mapping for standard gamepads +- ⌨️ **Keyboard Navigation** - Full keyboard navigation with customizable key bindings +- 🖱️ **Mouse/Touch Support** - Works seamlessly with mouse and touch interactions +- 🎯 **Action Binding System** - Flexible system for binding actions to inputs +- 📐 **Directional Navigation** - Smart directional navigation based on element positions +- 🎨 **Customizable Themes** - Support for different key name themes (PlayStation, Xbox, etc.) +- 🔄 **Hot-plugging** - Automatic detection of connected/disconnected devices +- 🎛️ **Multiple Schemes** - Support for multiple input schemes per device -## Publishing the Library - -Once the project is built, you can publish your library by following these steps: - -1. Navigate to the `dist` directory: - ```bash - cd dist/input - ``` - -2. Run the `npm publish` command to publish your library to the npm registry: - ```bash - npm publish - ``` - -## Running unit tests - -To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: +## Development +### Building ```bash -ng test +npm run build:input ``` -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - +### Watching for changes ```bash -ng e2e +npm run watch:input ``` -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. +### Testing +```bash +npm run test:input +``` -## Additional Resources +### Publishing +```bash +npm run publish:input +``` -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +## License + +This library is licensed under a custom license that allows: + +- **Free use** for non-commercial applications (no revenue, no ads, no paid features) +- **Commercial use** with attribution requirement - you must credit the library in your app (e.g., on an "About" or "For Developers" page) + +For full license terms, see the [LICENSE](LICENSE) file. + +### Attribution Example for Commercial Use + +``` +This application uses Workshack Input Library +(https://www.npmjs.com/package/@workshack/input) +© 2025 Workshack Team +``` diff --git a/package.json b/package.json index 26b33b9..212f39e 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,25 @@ { - "name": "@ngshack/input", - "version": "0.0.1", - "description": "Webland input components library", - "author": "Webland Team", - "license": "MIT", - "keywords": ["angular", "input", "forms", "components", "webland"], + "name": "@workshack/input", + "version": "1.0.0", + "description": "Workshack input components library with gamepad and keyboard navigation support", + "author": "Workshack Team", + "license": "SEE LICENSE IN LICENSE", + "keywords": ["angular", "input", "forms", "components", "workshack", "gamepad", "navigation", "gaming"], "repository": { "type": "git", - "url": "https://github.com/webland/webland.git", - "directory": "app/packages/input" + "url": "https://git.fufle.net/workshack/input.git", + "directory": "." }, - "homepage": "https://github.com/webland/webland#readme", + "homepage": "https://git.fufle.net/workshack/input", "bugs": { - "url": "https://github.com/webland/webland/issues" + "url": "https://git.fufle.net/workshack/input/issues" }, - "main": "bundles/webland-input.umd.js", - "module": "fesm2022/webland-input.mjs", + "main": "bundles/workshack-input.umd.js", + "module": "fesm2022/workshack-input.mjs", "typings": "index.d.ts", - "exports": { - "./package.json": { - "default": "./package.json" - }, - ".": { - "types": "./index.d.ts", - "esm": "./fesm2022/webland-input.mjs", - "default": "./fesm2022/webland-input.mjs" - } - }, "peerDependencies": { - "@angular/common": "^20.1.0", - "@angular/core": "^20.1.0" + "@angular/common": "^20.1.3", + "@angular/core": "^20.1.3" }, "dependencies": { "tslib": "^2.3.0" diff --git a/src/lib/modules/navigation/directives/key-item.directive.ts b/src/lib/modules/navigation/directives/key-item.directive.ts index 1371f4b..86a18df 100644 --- a/src/lib/modules/navigation/directives/key-item.directive.ts +++ b/src/lib/modules/navigation/directives/key-item.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, Optional, Host } from '@angular/core'; +import { Directive, ElementRef, HostListener, Input, OnDestroy, OnInit, Optional, Host, EventEmitter, Output } from '@angular/core'; import { KeyItemGroupDirective } from './key-item-group.directive'; export interface ActionBindings { @@ -28,11 +28,11 @@ interface HTMLKeyItemEntryElement extends HTMLElement { export class KeyItemDirective implements OnInit, OnDestroy { @Input() active = 'active'; - @Input() activate: () => any = () => {}; - @Input() deactivate: () => any = () => {}; - @Input() focus: () => any = () => {}; - @Input() blur: () => any = () => {}; @Input() actionBindings: ActionBindings = {}; + @Output() activated = new EventEmitter(); + @Output() deactivated = new EventEmitter(); + @Output() focused = new EventEmitter(); + @Output() blurred = new EventEmitter(); private static allItems: KeyItemEntry[] = []; private static current?: KeyItemEntry; private element: HTMLKeyItemEntryElement; @@ -40,7 +40,8 @@ export class KeyItemDirective implements OnInit, OnDestroy { private id = KeyItemDirective.NextId++; constructor( - @Optional() private group: KeyItemGroupDirective, + @Optional() + private group: KeyItemGroupDirective, elementRef: ElementRef, ) { this.element = elementRef.nativeElement as HTMLKeyItemEntryElement; @@ -57,12 +58,12 @@ export class KeyItemDirective implements OnInit, OnDestroy { ngOnInit() { this.element.active = this.active; - this.element.activate = this.activate; - this.element.deactivate = this.deactivate; + this.element.activate = () => this.activated.emit(); + this.element.deactivate = () => this.deactivated.emit(); this.element.keyId = this.id; this.element.actionBindings = this.actionBindings; - this.element.focus = this.focus; - this.element.blur = this.blur; + this.element.focus = () => this.focused.emit(); + this.element.blur = () => this.blurred.emit(); const position = this.element.getBoundingClientRect(); const item = { element: this.element, @@ -100,7 +101,7 @@ export class KeyItemDirective implements OnInit, OnDestroy { this.element.classList.add(this.active); } - this.focus(); + this.focused.emit(); const item = KeyItemDirective.allItems.find(item => item.element.keyId === this.id); KeyItemDirective.SetCurrentItem(item); @@ -112,7 +113,7 @@ export class KeyItemDirective implements OnInit, OnDestroy { this.element.classList.remove(this.active); } - this.blur(); + this.blurred.emit(); } static ActivateCurrentItem() {