Skip to content

Commit

Permalink
Merge pull request #335 from emilhe/1.0.17
Browse files Browse the repository at this point in the history
1.0.17
  • Loading branch information
emilhe authored Jun 28, 2024
2 parents 45a91c1 + 472e51d commit 56e58e3
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 58 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [1.0.17] - UNRELEASED

### Added

- Add the option to send objects to the `captureKeys` property of the `Keyboard` component to enable more specific filtering
- Add streaming capabilities to the `SSE` component

## [1.0.16] - 05-28-24

### Added
Expand Down
134 changes: 80 additions & 54 deletions src/lib/components/Keyboard.react.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from 'react';
import PropTypes from 'prop-types';
import { Component } from 'react';

/**
* The Keyboard component listens for keyboard events.
Expand All @@ -13,45 +13,71 @@ export default class Keyboard extends Component {
this.keyupHandler = this.keyupHandler.bind(this);
}

getSources(){
getSources() {
const sources = [...this.myRef.current.children];
if(sources.length === 0){
if (sources.length === 0) {
sources.push(document); // if no children are provided, attach to the document object.
}
return sources;
}

keydownHandler(event) {
if(!this.props.captureKeys || this.props.captureKeys.indexOf(event.key) > -1){
const keydown = this.props.eventProps.reduce(
function(o, k) { o[k] = event[k]; return o; }, {})
this.props.setProps({keydown: keydown})
this.props.setProps({n_keydowns: this.props.n_keydowns + 1})
if(keydown.key){
const keys_pressed = Object.assign(this.props.keys_pressed, {})
keys_pressed[keydown.key] = keydown
this.props.setProps({keys_pressed: keys_pressed})
filterEvents(event) {
if (!this.props.captureKeys) {
return true;
}
for (const captureKey of this.props.captureKeys) {
// if captureKey is a string, check if it matches the event key
if (typeof captureKey === 'string' || captureKey instanceof String) {
if (captureKey === event.key) {
return true;
}
continue;
}
// if captureKey is an object, check if it matches all keys
let fullMatch = true
for (const [key, value] of Object.entries(captureKey)) {
fullMatch = fullMatch && value === event[key];
}
if (fullMatch) {
return true;
}
}
return false;
}

keydownHandler(event) {
if (!this.filterEvents(event)) {
return;
}
const keydown = this.props.eventProps.reduce(
function (o, k) { o[k] = event[k]; return o; }, {})
this.props.setProps({ keydown: keydown })
this.props.setProps({ n_keydowns: this.props.n_keydowns + 1 })
if (keydown.key) {
const keys_pressed = Object.assign(this.props.keys_pressed, {})
keys_pressed[keydown.key] = keydown
this.props.setProps({ keys_pressed: keys_pressed })
}
}

keyupHandler(event) {
if(!this.props.captureKeys || this.props.captureKeys.indexOf(event.key) > -1){
const keyup = this.props.eventProps.reduce(
function(o, k) { o[k] = event[k]; return o; }, {})
this.props.setProps({keyup: keyup})
this.props.setProps({n_keyups: this.props.n_keyups + 1})
if(keyup.key){
const keys_pressed = Object.assign(this.props.keys_pressed, {})
delete keys_pressed[event.key];
this.props.setProps({keys_pressed: keys_pressed})
}
if (!this.filterEvents(event)) {
return;
}
const keyup = this.props.eventProps.reduce(
function (o, k) { o[k] = event[k]; return o; }, {})
this.props.setProps({ keyup: keyup })
this.props.setProps({ n_keyups: this.props.n_keyups + 1 })
if (keyup.key) {
const keys_pressed = Object.assign(this.props.keys_pressed, {})
delete keys_pressed[event.key];
this.props.setProps({ keys_pressed: keys_pressed })
}
}

componentDidMount() {
this.getSources().forEach(s => s.addEventListener("keydown", this.keydownHandler, this.props.useCapture));
this.getSources().forEach(s => s.addEventListener("keyup", this.keyupHandler, this.props.useCapture));
this.getSources().forEach(s => s.addEventListener("keyup", this.keyupHandler, this.props.useCapture));
}

componentWillUnmount() {
Expand All @@ -61,13 +87,13 @@ export default class Keyboard extends Component {

render() {
return <div className={this.props.className} style={this.props.style} ref={this.myRef}>
{this.props.children}
</div>;
{this.props.children}
</div>;
}
};

Keyboard.defaultProps = {
eventProps: ["key", "altKey", "ctrlKey", "shiftKey","metaKey", "repeat"],
eventProps: ["key", "altKey", "ctrlKey", "shiftKey", "metaKey", "repeat"],
n_keydowns: 0,
n_keyups: 0,
keys_pressed: {},
Expand All @@ -76,10 +102,10 @@ Keyboard.defaultProps = {


Keyboard.propTypes = {
/**
* The children of this component. If any children are provided, the component will listen for events from these
components. If no children are specified, the component will listen for events from the document object.
*/
/**
* The children of this component. If any children are provided, the component will listen for events from these
components. If no children are specified, the component will listen for events from the document object.
*/
children: PropTypes.node,

/**
Expand All @@ -103,47 +129,47 @@ Keyboard.propTypes = {
eventProps: PropTypes.arrayOf(PropTypes.string),

/**
* The keys to capture. Defaults to all keys.
* The keys to capture. Defaults to all keys. Can be either a string (e.g. "Enter") or an object (e.g. "{key: 'Enter', ctrlKey: true}").
*/
captureKeys: PropTypes.arrayOf(PropTypes.string),
captureKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),

/**
* Dash-assigned callback that should be called to report property changes
* to Dash, to make them available for callbacks.
*/
setProps: PropTypes.func,

/**
* keydown (dict) the object that holds the result of the key down event. It is a dictionary with the following keys:
* "key", "altKey", "ctrlKey", "shiftKey","metaKey", "repeat". Those keys have the following values:
*
* - key (str) which key is pressed
* - altKey (bool) whether the Alt key is pressed
* - ctrlKey (bool) Ctrl key is pressed
* - shiftKey (bool) Shift key is pressed
* - metaKey (bool) Meta key is pressed (Mac: Command key or PC: Windows key)
* - repeat (bool) whether the key is held down
*/
/**
* keydown (dict) the object that holds the result of the key down event. It is a dictionary with the following keys:
* "key", "altKey", "ctrlKey", "shiftKey","metaKey", "repeat". Those keys have the following values:
*
* - key (str) which key is pressed
* - altKey (bool) whether the Alt key is pressed
* - ctrlKey (bool) Ctrl key is pressed
* - shiftKey (bool) Shift key is pressed
* - metaKey (bool) Meta key is pressed (Mac: Command key or PC: Windows key)
* - repeat (bool) whether the key is held down
*/
keydown: PropTypes.object,

/**
* keyup (dict) the object that holds the result of the key up event. Structure like keydown.
*/
/**
* keyup (dict) the object that holds the result of the key up event. Structure like keydown.
*/
keyup: PropTypes.object,

/**
* keys_pressed (dict) is a dict of objects like keydown for all keys currently pressed.
*/
keys_pressed: PropTypes.object,

/**
* A counter, which is incremented on each key down event, similar to n_clicks for buttons.
*/
n_keydowns: PropTypes.number,
/**
* A counter, which is incremented on each key down event, similar to n_clicks for buttons.
*/
n_keydowns: PropTypes.number,

/**
* A counter, which is incremented on each key up event, similar to n_clicks for buttons.
*/
/**
* A counter, which is incremented on each key up event, similar to n_clicks for buttons.
*/
n_keyups: PropTypes.number,

/**
Expand Down
78 changes: 74 additions & 4 deletions src/lib/components/SSE.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { SSE as SSEjs } from "sse.js";
* The SSE component makes it possible to collect data from e.g. a ResponseStream. It's a wrapper around the SSE.js library.
* https://github.com/mpetazzoni/sse.js
*/
const SSE = ({ url, options, concat, setProps }) => {
const SSE = ({ url, options, concat, animate_delay, animate_chunk, animate_prefix, animate_suffix, setProps, done }) => {
const [data, setData] = useState("");
const [animateData, setAnimateData] = useState("");
const animate = animate_delay > 0 && animate_chunk > 0;

useEffect(() => {
// Reset on url change.
setProps({ done: false })
setData("")
setAnimateData("")
// Don't do anything if url is not set.
if (!url) { return () => { }; }
// Instantiate EventSource.
Expand Down Expand Up @@ -41,14 +44,56 @@ const SSE = ({ url, options, concat, setProps }) => {
};
}, [url, options]);

// Update value.
setProps({ value: data })
useEffect(() => {
// Don't animate if not set.
if (!animate) { return () => { }; };
// Apply prefix/suffix filters.
let filteredData = data;
if (animate_prefix) {
if (!data.includes(animate_prefix)) {
return () => { };
}
filteredData = filteredData.slice(animate_prefix.length)
}
if (filteredData.includes(animate_suffix)) {
filteredData = filteredData.split(animate_suffix)[0]
}
// If done, animate the whole data.
if (done) {
setAnimateData(filteredData);
return () => { };
};
// If there is not data, just return.
if (filteredData.length === 0) {
return () => { };
};
// Animate data.
let buffer = animateData;
const interval = setInterval(() => {
// If we're done, stop the interval.
if (buffer.length >= filteredData.length) {
clearInterval(interval);
}
// Otherwise, move to the next chunk.
const endIdx = Math.min(buffer.length + animate_chunk, filteredData.length);
buffer = filteredData.slice(0, endIdx);
setAnimateData(buffer);
}, animate_delay);
return () => clearInterval(interval);
}, [data, done]);

// Update value(s).
setProps({ animation: animateData });
setProps({ value: data });

// Don't render anything.
return <></>;
}

SSE.defaultProps = {
concat: true,
animate_delay: 0,
animate_chunk: 1,
};

SSE.propTypes = {
Expand Down Expand Up @@ -87,10 +132,35 @@ SSE.propTypes = {
concat: PropTypes.bool,

/**
* The data value. Either the latest, or the concatenated dependenig on the `concat` property.
* The data value. Either the latest, or the concatenated depending on the `concat` property.
*/
value: PropTypes.string,

/**
* If set, each character is delayed by some amount of time. Used to animate the stream.
*/
animate_delay: PropTypes.number,

/**
* Chunk size (i.e. number of characters) for the animation.
*/
animate_chunk: PropTypes.number,

/**
* Prefix to be excluded from the animation.
*/
animate_prefix: PropTypes.string,

/**
* Suffix to be excluded from the animation.
*/
animate_suffix: PropTypes.string,

/**
* The data value. Either the latest, or the concatenated depending on the `concat` property.
*/
animation: PropTypes.string,

/**
* A boolean indicating if the (current) stream has ended.
*/
Expand Down

0 comments on commit 56e58e3

Please sign in to comment.