KeyItem event bindings

This commit is contained in:
Michał Sieciechowicz 2025-08-02 18:57:16 +02:00
parent 2a8d0269b8
commit 88918c7a57
6 changed files with 527 additions and 130 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

31
.npmignore Normal file
View File

@ -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*

55
LICENSE Normal file
View File

@ -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.

507
README.md
View File

@ -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: `
<div inputActionBinding="confirm" (action)="onConfirm()">
<button>Confirm</button>
</div>
`
// ...
})
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<InputDevice[]>` - 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: `
<div inputActionBinding="back" (action)="goBack()">
Back Button
</div>
`
// ...
})
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
<button inputActionBinding="confirm">Confirm</button>
<div inputActionBinding="back">Back</div>
```
**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
<button
inputKeyItem
[active]="'focused'"
[actionBindings]="{
Accept: () => onClick(),
Back: () => onCancel()
}"
(activated)="onActivated()"
(focused)="onFocused()"
(blurred)="onBlurred()">
Click me
</button>
```
**Properties:**
- `active: string` - CSS class name for active/focused state (default: 'active')
- `actionBindings: ActionBindings` - Object mapping action names to callback functions
**Events:**
- `activated: EventEmitter<void>` - Emitted when item is activated
- `deactivated: EventEmitter<void>` - Emitted when item is deactivated
- `focused: EventEmitter<void>` - Emitted when item receives focus
- `blurred: EventEmitter<void>` - 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
<div inputKeyItemGroup>
<button inputKeyItem>Button 1</button>
<button inputKeyItem>Button 2</button>
<button inputKeyItem>Button 3</button>
</div>
```
**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<InputGamepad>` - Observable for gamepad connections
- `DeviceDisconnected: Subject<InputGamepad>` - 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: `
<div class="game-menu" inputKeyItemGroup>
<h2>Game Menu</h2>
<button
inputKeyItem
[actionBindings]="{
Accept: () => startGame(),
Back: () => goBack()
}"
(focused)="playHoverSound()"
class="menu-item">
Start Game
</button>
<button
inputKeyItem
[actionBindings]="{
Accept: () => showOptions(),
Back: () => goBack()
}"
(focused)="playHoverSound()"
class="menu-item">
Options
</button>
<button
inputKeyItem
[actionBindings]="{
Accept: () => exitGame(),
Back: () => goBack()
}"
(focused)="playHoverSound()"
class="menu-item">
Exit
</button>
<div class="controls-hint">
<span>Use arrow keys or gamepad to navigate</span>
<span>Press Enter or A to select</span>
</div>
</div>
`,
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
```

View File

@ -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"

View File

@ -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<void>();
@Output() deactivated = new EventEmitter<void>();
@Output() focused = new EventEmitter<void>();
@Output() blurred = new EventEmitter<void>();
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() {