lively 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,58 +1,174 @@
1
- # Live (JavaScript)
1
+ # Live.js
2
2
 
3
- This is the JavaScript library for implementing the Ruby gem of the same name.
3
+ A JavaScript client library for building interactive web applications with Ruby Live framework.
4
4
 
5
- ## Document Manipulation
5
+ [![Development Status](https://github.com/socketry/live-js/workflows/Test/badge.svg)](https://github.com/socketry/live-js/actions?workflow=Test)
6
6
 
7
- ### `live.update(id, html, options)`
7
+ ## Features
8
8
 
9
- Updates the content of the element with the given `id` with the given `html`. The `options` parameter is optional and can be used to pass additional options to the update method.
9
+ - **Real-time Communication**: WebSocket-based client-server communication.
10
+ - **DOM Manipulation**: Efficient updating, replacing, and modifying HTML elements.
11
+ - **Event Forwarding**: Forward client events to server for processing.
12
+ - **Controller Loading**: Declarative JavaScript controller loading with `data-live-controller`.
13
+ - **Automatic Cleanup**: Proper lifecycle management and memory cleanup.
14
+ - **Live Elements**: Automatic binding and unbinding of live elements.
10
15
 
11
- - `options.reply` - If truthy, the server will reply with `{reply: options.reply}`.
16
+ ## Usage
12
17
 
13
- ### `live.replace(selector, html, options)`
18
+ ### Installation
14
19
 
15
- Replaces the element(s) selected by the given `selector` with the given `html`. The `options` parameter is optional and can be used to pass additional options to the replace method.
20
+ ```bash
21
+ npm install @socketry/live
22
+ ```
16
23
 
17
- - `options.reply` - If truthy, the server will reply with `{reply: options.reply}`.
24
+ ### Basic Setup
18
25
 
19
- ### `live.prepend(selector, html, options)`
26
+ ```javascript
27
+ import { Live } from '@socketry/live';
20
28
 
21
- Prepends the given `html` to the element(s) selected by the given `selector`. The `options` parameter is optional and can be used to pass additional options to the prepend method.
29
+ // Start the live connection
30
+ const live = Live.start({
31
+ path: 'live', // WebSocket endpoint
32
+ base: window.location.href
33
+ });
34
+ ```
22
35
 
23
- - `options.reply` - If truthy, the server will reply with `{reply: options.reply}`.
36
+ ### Controller Loading
24
37
 
25
- ### `live.append(selector, html, options)`
38
+ Live.js supports declarative controller loading using the `data-live-controller` attribute:
26
39
 
27
- Appends the given `html` to the element(s) selected by the given `selector`. The `options` parameter is optional and can be used to pass additional options to the append method.
40
+ ```html
41
+ <div class="live" id="game" data-live-controller="/static/game_controller.mjs">
42
+ <!-- Game content -->
43
+ </div>
44
+ ```
28
45
 
29
- - `options.reply` - If truthy, the server will reply with `{reply: options.reply}`.
46
+ ```javascript
47
+ // game_controller.mjs
48
+ export default function(element) {
49
+ console.log('Controller loaded for:', element);
50
+
51
+ // Setup your controller logic
52
+ element.addEventListener('click', handleClick);
53
+
54
+ // Return a controller object with cleanup
55
+ return {
56
+ dispose() {
57
+ element.removeEventListener('click', handleClick);
58
+ }
59
+ };
60
+ }
61
+ ```
30
62
 
31
- ### `live.remove(selector, options)`
63
+ ## API Reference
32
64
 
33
- Removes the element(s) selected by the given `selector`. The `options` parameter is optional and can be used to pass additional options to the remove method.
65
+ ### Live Class
34
66
 
35
- - `options.reply` - If truthy, the server will reply with `{reply: options.reply}`.
67
+ #### Static Methods
36
68
 
37
- ### `live.dispatchEvent(selector, type, options)`
69
+ - `Live.start(options)` - Create and start a new Live instance
70
+ - `options.window` - Window object (defaults to globalThis)
71
+ - `options.path` - WebSocket path (defaults to 'live')
72
+ - `options.base` - Base URL (defaults to window.location.href)
38
73
 
39
- Dispatches an event of the given `type` on the element(s) selected by the given `selector`. The `options` parameter is optional and can be used to pass additional options to the dispatchEvent method.
74
+ #### Instance Methods
40
75
 
41
- - `options.detail` - The detail object to pass to the event.
42
- - `options.bubbles` - A boolean indicating whether the event should bubble up through the DOM.
43
- - `options.cancelable` - A boolean indicating whether the event can be canceled.
44
- - `options.composed` - A boolean indicating whether the event will trigger listeners outside of a shadow root.
76
+ ##### Connection Management
77
+ - `connect()` - Establish WebSocket connection
78
+ - `disconnect()` - Close WebSocket connection
45
79
 
46
- ## Event Handling
80
+ ##### DOM Manipulation
81
+ - `update(id, html, options)` - Update element content
82
+ - `replace(selector, html, options)` - Replace elements
83
+ - `prepend(selector, html, options)` - Prepend content
84
+ - `append(selector, html, options)` - Append content
85
+ - `remove(selector, options)` - Remove elements
47
86
 
48
- ### `live.forward(id, event)`
87
+ ##### Event Handling
88
+ - `forward(id, event)` - Forward event to server
89
+ - `forwardEvent(id, event, detail, preventDefault)` - Forward DOM event
90
+ - `forwardFormEvent(id, event, detail, preventDefault)` - Forward form event
49
91
 
50
- Connect and forward an event on the element with the given `id`. If the connection can't be established, the event will be buffered.
92
+ ##### Script Execution
93
+ - `script(id, code, options)` - Execute JavaScript code
94
+ - `loadController(id, path, options)` - Load JavaScript controller
51
95
 
52
- ### `live.forwardEvent(id, event, details)`
96
+ ##### Event Dispatching
97
+ - `dispatchEvent(selector, type, options)` - Dispatch custom events
53
98
 
54
- Forward a HTML DOM event to the server. The `details` parameter is optional and can be used to pass additional details to the server.
99
+ ### Options Parameter
55
100
 
56
- ### `live.forwardFormEvent(id, event, details)`
101
+ Most methods accept an `options` parameter with:
102
+ - `options.reply` - If truthy, server will reply with `{reply: options.reply}`
57
103
 
58
- Forward an event which has form data to the server. The `details` parameter is optional and can be used to pass additional details to the server.
104
+ ### Controller Pattern
105
+
106
+ Controllers are JavaScript modules that manage view-specific behavior:
107
+
108
+ ```javascript
109
+ // Simple controller
110
+ export default function(element) {
111
+ // Setup code
112
+ return {
113
+ dispose() {
114
+ // Cleanup code
115
+ }
116
+ };
117
+ }
118
+
119
+ // With options
120
+ export default function(element, options) {
121
+ const config = options.config || {};
122
+ // Use config...
123
+ }
124
+ ```
125
+
126
+ ## Live Elements
127
+
128
+ Elements with the `live` CSS class are automatically managed:
129
+
130
+ ```html
131
+ <div class="live" id="my-element">
132
+ Content that can be updated
133
+ </div>
134
+ ```
135
+
136
+ ## Event Examples
137
+
138
+ ### Basic Event Forwarding
139
+
140
+ ```javascript
141
+ // Forward click events
142
+ element.addEventListener('click', (event) => {
143
+ live.forwardEvent('my-element', event, { button: 'clicked' });
144
+ });
145
+
146
+ // Forward form submissions
147
+ form.addEventListener('submit', (event) => {
148
+ live.forwardFormEvent('my-form', event, { action: 'submit' });
149
+ });
150
+ ```
151
+
152
+ ## Contributing
153
+
154
+ We welcome contributions to this project.
155
+
156
+ 1. Fork it.
157
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
158
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
159
+ 4. Push to the branch (`git push origin my-new-feature`).
160
+ 5. Create new Pull Request.
161
+
162
+ ### Developer Certificate of Origin
163
+
164
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
165
+
166
+ ### Community Guidelines
167
+
168
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
169
+
170
+ ## See Also
171
+
172
+ - [lively](https://github.com/socketry/lively) — Ruby framework for building interactive web applications.
173
+ - [live](https://github.com/socketry/live) — Provides client-server communication using websockets.
174
+ - [live-audio-js](https://github.com/socketry/live-audio-js) — Web Audio API-based game audio synthesis library.
@@ -0,0 +1,168 @@
1
+ // Audio Controller - manages sound instances and provides unified API
2
+ import { Output } from './Output.js';
3
+ import { Sound } from './Sound.js';
4
+
5
+ // Get or create shared AudioContext (keyed by window)
6
+ async function getSharedAudioContext(window = globalThis) {
7
+ const contextKey = '_liveAudioContext';
8
+
9
+ let audioContext = window[contextKey];
10
+
11
+ if (!audioContext || audioContext.state === 'closed') {
12
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({
13
+ latencyHint: 'interactive',
14
+ });
15
+
16
+ if (audioContext.state === 'suspended') {
17
+ await audioContext.resume();
18
+ }
19
+
20
+ window[contextKey] = audioContext;
21
+
22
+ console.log(`Live Audio Context created - Sample rate: ${audioContext.sampleRate}Hz, State: ${audioContext.state}`);
23
+ }
24
+
25
+ return audioContext;
26
+ }
27
+
28
+ export class Controller {
29
+ #window = null;
30
+ #audioContext = null;
31
+ #output = null;
32
+ #sounds = {};
33
+ #volume = 1.0;
34
+
35
+ // Callbacks:
36
+ #onOutputCreated = null;
37
+ #onOutputDisposed = null;
38
+
39
+ constructor(window = globalThis, options = {}) {
40
+ this.#window = window;
41
+ this.#onOutputCreated = options.onOutputCreated || null;
42
+ this.#onOutputDisposed = options.onOutputDisposed || null;
43
+ }
44
+
45
+ // Acquire output with AudioContext ready - returns null if not available.
46
+ async acquireOutput() {
47
+ let output = this.#output;
48
+
49
+ if (!output) {
50
+ // First get the AudioContext at Controller level
51
+ const audioContext = await getSharedAudioContext(this.#window);
52
+ if (!audioContext) return null;
53
+
54
+ // Then create Output instance with AudioContext:
55
+ this.#output = output = new Output(audioContext);
56
+
57
+ // Apply the controller's volume to the new output
58
+ output.setVolume(this.#volume);
59
+
60
+ // Call the output created callback if provided
61
+ if (this.#onOutputCreated) {
62
+ this.#onOutputCreated(this, output);
63
+ }
64
+ }
65
+
66
+ return output;
67
+ }
68
+
69
+ // Add a sound to this controller instance
70
+ addSound(name, value) {
71
+ this.#sounds[name] = value;
72
+ return value;
73
+ }
74
+
75
+ // Play a sound by name
76
+ async playSound(name) {
77
+ // Return early if volume is zero (muted)
78
+ if (this.#volume <= 0) return;
79
+
80
+ const sound = this.#sounds[name];
81
+ if (sound) {
82
+ const output = await this.acquireOutput();
83
+ if (!output) return;
84
+ sound.play(output);
85
+ } else {
86
+ console.warn(`Sound '${name}' not found`);
87
+ }
88
+ }
89
+
90
+ // Stop a sound by name
91
+ stopSound(name) {
92
+ const sound = this.#sounds[name];
93
+ if (sound) {
94
+ sound.stop();
95
+ } else {
96
+ console.warn(`Sound '${name}' not found`);
97
+ }
98
+ }
99
+
100
+ // Stop all sounds
101
+ stopAllSounds() {
102
+ if (this.#sounds) {
103
+ Object.values(this.#sounds).forEach(sound => sound.stop());
104
+ }
105
+ }
106
+
107
+ // Get a sound instance for direct access
108
+ getSound(name) {
109
+ return this.#sounds[name];
110
+ }
111
+
112
+ // List all available sound names
113
+ listSounds() {
114
+ return Object.keys(this.#sounds);
115
+ }
116
+
117
+ // Remove a sound
118
+ removeSound(name) {
119
+ if (this.#sounds[name]) {
120
+ delete this.#sounds[name];
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ // Set master volume
127
+ async setVolume(volume) {
128
+ this.#volume = volume;
129
+
130
+ // Apply to output if it exists, or acquire it
131
+ const output = await this.acquireOutput();
132
+ if (output) {
133
+ output.setVolume(volume);
134
+ }
135
+ }
136
+
137
+ // Get current volume
138
+ get volume() {
139
+ return this.#volume;
140
+ }
141
+
142
+ // Get sounds object (for testing)
143
+ get sounds() {
144
+ return this.#sounds;
145
+ }
146
+
147
+ // Get window object (for testing)
148
+ get window() {
149
+ return this.#window;
150
+ }
151
+
152
+ // Dispose of the controller and clean up resources
153
+ dispose() {
154
+ if (this.#output) {
155
+ const output = this.#output;
156
+
157
+ // Call the disposal callback if provided
158
+ if (this.#onOutputDisposed) {
159
+ this.#onOutputDisposed(this, output);
160
+ }
161
+
162
+ output.dispose();
163
+ this.#output = null;
164
+ }
165
+
166
+ this.#sounds = {};
167
+ }
168
+ }