KeyItem event bindings
This commit is contained in:
parent
2a8d0269b8
commit
88918c7a57
|
@ -0,0 +1 @@
|
||||||
|
cache
|
|
@ -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*
|
|
@ -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
507
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
|
```bash
|
||||||
npm install @ngshack/input
|
npm install @workshack/input
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Services
|
||||||
|
|
||||||
### Prerequisites
|
### InputService
|
||||||
- Node.js 18+
|
|
||||||
- Angular CLI 17+
|
|
||||||
|
|
||||||
### Setup
|
**Description:** Core service for managing input devices, actions, and schemes.
|
||||||
```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
|
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
```typescript
|
```typescript
|
||||||
import { NavigationModule } from '@ngshack/input';
|
import { InputService } from '@workshack/input';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [NavigationModule],
|
// ...
|
||||||
template: `
|
|
||||||
<div inputActionBinding="confirm" (action)="onConfirm()">
|
|
||||||
<button>Confirm</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class MyComponent {
|
export class MyComponent {
|
||||||
onConfirm() {
|
constructor(private inputService: InputService) {
|
||||||
console.log('Confirmed!');
|
// 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
|
```typescript
|
||||||
import { ActionBindingDirective } from '@ngshack/input';
|
import { ControlService } from 'your-app/services/control.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [ActionBindingDirective],
|
// ...
|
||||||
template: `
|
|
||||||
<div inputActionBinding="back" (action)="goBack()">
|
|
||||||
Back Button
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class MyComponent {
|
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() {
|
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
|
## Features
|
||||||
|
|
||||||
- 🎮 Gamepad support
|
- 🎮 **Gamepad Support** - Automatic detection and mapping for standard gamepads
|
||||||
- ⌨️ Keyboard navigation
|
- ⌨️ **Keyboard Navigation** - Full keyboard navigation with customizable key bindings
|
||||||
- 🖱️ Mouse/touch support
|
- 🖱️ **Mouse/Touch Support** - Works seamlessly with mouse and touch interactions
|
||||||
- 🎯 Action binding system
|
- 🎯 **Action Binding System** - Flexible system for binding actions to inputs
|
||||||
- 📱 Responsive design
|
- 📐 **Directional Navigation** - Smart directional navigation based on element positions
|
||||||
- 🎨 Gaming console theme
|
- 🎨 **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
|
## Development
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
|
### Building
|
||||||
```bash
|
```bash
|
||||||
ng test
|
npm run build:input
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running end-to-end tests
|
### Watching for changes
|
||||||
|
|
||||||
For end-to-end (e2e) testing, run:
|
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
|
```
|
||||||
|
|
38
package.json
38
package.json
|
@ -1,35 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "@ngshack/input",
|
"name": "@workshack/input",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "Webland input components library",
|
"description": "Workshack input components library with gamepad and keyboard navigation support",
|
||||||
"author": "Webland Team",
|
"author": "Workshack Team",
|
||||||
"license": "MIT",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"keywords": ["angular", "input", "forms", "components", "webland"],
|
"keywords": ["angular", "input", "forms", "components", "workshack", "gamepad", "navigation", "gaming"],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/webland/webland.git",
|
"url": "https://git.fufle.net/workshack/input.git",
|
||||||
"directory": "app/packages/input"
|
"directory": "."
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/webland/webland#readme",
|
"homepage": "https://git.fufle.net/workshack/input",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/webland/webland/issues"
|
"url": "https://git.fufle.net/workshack/input/issues"
|
||||||
},
|
},
|
||||||
"main": "bundles/webland-input.umd.js",
|
"main": "bundles/workshack-input.umd.js",
|
||||||
"module": "fesm2022/webland-input.mjs",
|
"module": "fesm2022/workshack-input.mjs",
|
||||||
"typings": "index.d.ts",
|
"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": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^20.1.0",
|
"@angular/common": "^20.1.3",
|
||||||
"@angular/core": "^20.1.0"
|
"@angular/core": "^20.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
|
|
|
@ -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';
|
import { KeyItemGroupDirective } from './key-item-group.directive';
|
||||||
|
|
||||||
export interface ActionBindings {
|
export interface ActionBindings {
|
||||||
|
@ -28,11 +28,11 @@ interface HTMLKeyItemEntryElement extends HTMLElement {
|
||||||
export class KeyItemDirective implements OnInit, OnDestroy {
|
export class KeyItemDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@Input() active = 'active';
|
@Input() active = 'active';
|
||||||
@Input() activate: () => any = () => {};
|
|
||||||
@Input() deactivate: () => any = () => {};
|
|
||||||
@Input() focus: () => any = () => {};
|
|
||||||
@Input() blur: () => any = () => {};
|
|
||||||
@Input() actionBindings: ActionBindings = {};
|
@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 allItems: KeyItemEntry[] = [];
|
||||||
private static current?: KeyItemEntry;
|
private static current?: KeyItemEntry;
|
||||||
private element: HTMLKeyItemEntryElement;
|
private element: HTMLKeyItemEntryElement;
|
||||||
|
@ -40,7 +40,8 @@ export class KeyItemDirective implements OnInit, OnDestroy {
|
||||||
private id = KeyItemDirective.NextId++;
|
private id = KeyItemDirective.NextId++;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() private group: KeyItemGroupDirective,
|
@Optional()
|
||||||
|
private group: KeyItemGroupDirective,
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
) {
|
) {
|
||||||
this.element = elementRef.nativeElement as HTMLKeyItemEntryElement;
|
this.element = elementRef.nativeElement as HTMLKeyItemEntryElement;
|
||||||
|
@ -57,12 +58,12 @@ export class KeyItemDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.element.active = this.active;
|
this.element.active = this.active;
|
||||||
this.element.activate = this.activate;
|
this.element.activate = () => this.activated.emit();
|
||||||
this.element.deactivate = this.deactivate;
|
this.element.deactivate = () => this.deactivated.emit();
|
||||||
this.element.keyId = this.id;
|
this.element.keyId = this.id;
|
||||||
this.element.actionBindings = this.actionBindings;
|
this.element.actionBindings = this.actionBindings;
|
||||||
this.element.focus = this.focus;
|
this.element.focus = () => this.focused.emit();
|
||||||
this.element.blur = this.blur;
|
this.element.blur = () => this.blurred.emit();
|
||||||
const position = this.element.getBoundingClientRect();
|
const position = this.element.getBoundingClientRect();
|
||||||
const item = {
|
const item = {
|
||||||
element: this.element,
|
element: this.element,
|
||||||
|
@ -100,7 +101,7 @@ export class KeyItemDirective implements OnInit, OnDestroy {
|
||||||
this.element.classList.add(this.active);
|
this.element.classList.add(this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focus();
|
this.focused.emit();
|
||||||
|
|
||||||
const item = KeyItemDirective.allItems.find(item => item.element.keyId === this.id);
|
const item = KeyItemDirective.allItems.find(item => item.element.keyId === this.id);
|
||||||
KeyItemDirective.SetCurrentItem(item);
|
KeyItemDirective.SetCurrentItem(item);
|
||||||
|
@ -112,7 +113,7 @@ export class KeyItemDirective implements OnInit, OnDestroy {
|
||||||
this.element.classList.remove(this.active);
|
this.element.classList.remove(this.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blur();
|
this.blurred.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
static ActivateCurrentItem() {
|
static ActivateCurrentItem() {
|
||||||
|
|
Loading…
Reference in New Issue