jspsych 8.2.3 → 8.3.0

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.
package/dist/index.d.ts CHANGED
@@ -588,6 +588,7 @@ interface GetKeyboardResponseOptions {
588
588
  audio_context_start_time?: number;
589
589
  allow_held_key?: boolean;
590
590
  minimum_valid_rt?: number;
591
+ wait_for_key_release?: boolean;
591
592
  }
592
593
  declare class KeyboardListenerAPI {
593
594
  private getRootElement;
@@ -596,6 +597,8 @@ declare class KeyboardListenerAPI {
596
597
  constructor(getRootElement: () => Element | undefined, areResponsesCaseSensitive?: boolean, minimumValidRt?: number);
597
598
  private listeners;
598
599
  private heldKeys;
600
+ private keyDownTimestamps;
601
+ private pendingReleases;
599
602
  private areRootListenersRegistered;
600
603
  /**
601
604
  * If not previously done and `this.getRootElement()` returns an element, adds the root key
@@ -604,9 +607,24 @@ declare class KeyboardListenerAPI {
604
607
  private registerRootListeners;
605
608
  private rootKeydownListener;
606
609
  private toLowerCaseIfInsensitive;
610
+ /**
611
+ * identifies physical key of a keyboard event, for matching a keydown event with
612
+ * its corresponding keyup event, in the case of a change of shift state while
613
+ * the key is being held.
614
+ */
615
+ private getPhysicalKey;
607
616
  private rootKeyupListener;
617
+ /**
618
+ * When the window loses focus (`blur`), the browser will not deliver the `keyup` events for keys
619
+ * that are currently held, which would otherwise leave stale entries in `heldKeys` and
620
+ * `keyDownTimestamps` (making a key look permanently held) and orphan any pending releases. Treat
621
+ * the blur as a release of every held key: resolve each pending release with
622
+ * `rt_key_duration: null`, since the true hold duration cannot be measured once the release is
623
+ * never observed, then clear the tracked key state.
624
+ */
625
+ private rootBlurListener;
608
626
  private isResponseValid;
609
- getKeyboardResponse({ callback_function, valid_responses, rt_method, persist, audio_context, audio_context_start_time, allow_held_key, minimum_valid_rt, }: GetKeyboardResponseOptions): KeyboardListener;
627
+ getKeyboardResponse({ callback_function, valid_responses, rt_method, persist, audio_context, audio_context_start_time, allow_held_key, minimum_valid_rt, wait_for_key_release, }: GetKeyboardResponseOptions): KeyboardListener;
610
628
  cancelKeyboardResponse(listener: KeyboardListener): void;
611
629
  cancelAllKeyboardResponses(): void;
612
630
  compareKeys(key1: string | null, key2: string | null): boolean;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import autoBind from 'auto-bind';
2
2
  import rw from 'random-words';
3
3
  import seedrandom from 'seedrandom/lib/alea.js';
4
4
 
5
- var version = "8.2.3";
5
+ var version = "8.3.0";
6
6
 
7
7
  class ExtensionManager {
8
8
  constructor(dependencies, extensionsConfiguration) {
@@ -555,6 +555,8 @@ class KeyboardListenerAPI {
555
555
  this.minimumValidRt = minimumValidRt;
556
556
  this.listeners = /* @__PURE__ */ new Set();
557
557
  this.heldKeys = /* @__PURE__ */ new Set();
558
+ this.keyDownTimestamps = /* @__PURE__ */ new Map();
559
+ this.pendingReleases = /* @__PURE__ */ new Map();
558
560
  this.areRootListenersRegistered = false;
559
561
  autoBind(this);
560
562
  this.registerRootListeners();
@@ -569,11 +571,16 @@ class KeyboardListenerAPI {
569
571
  if (rootElement) {
570
572
  rootElement.addEventListener("keydown", this.rootKeydownListener);
571
573
  rootElement.addEventListener("keyup", this.rootKeyupListener);
574
+ window.addEventListener("blur", this.rootBlurListener);
572
575
  this.areRootListenersRegistered = true;
573
576
  }
574
577
  }
575
578
  }
576
579
  rootKeydownListener(e) {
580
+ const physicalKey = this.getPhysicalKey(e);
581
+ if (!this.keyDownTimestamps.has(physicalKey)) {
582
+ this.keyDownTimestamps.set(physicalKey, performance.now());
583
+ }
577
584
  for (const listener of [...this.listeners]) {
578
585
  listener(e);
579
586
  }
@@ -582,9 +589,43 @@ class KeyboardListenerAPI {
582
589
  toLowerCaseIfInsensitive(string) {
583
590
  return this.areResponsesCaseSensitive ? string : string.toLowerCase();
584
591
  }
592
+ /**
593
+ * identifies physical key of a keyboard event, for matching a keydown event with
594
+ * its corresponding keyup event, in the case of a change of shift state while
595
+ * the key is being held.
596
+ */
597
+ getPhysicalKey(e) {
598
+ return e.code || this.toLowerCaseIfInsensitive(e.key);
599
+ }
585
600
  rootKeyupListener(e) {
601
+ const physicalKey = this.getPhysicalKey(e);
602
+ for (const [listener, pending] of this.pendingReleases) {
603
+ if (pending.code === physicalKey) {
604
+ const pressTimestamp = this.keyDownTimestamps.get(physicalKey);
605
+ const rt_key_duration = pressTimestamp === void 0 ? null : Math.round(performance.now() - pressTimestamp);
606
+ this.pendingReleases.delete(listener);
607
+ pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration });
608
+ }
609
+ }
610
+ this.keyDownTimestamps.delete(physicalKey);
586
611
  this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));
587
612
  }
613
+ /**
614
+ * When the window loses focus (`blur`), the browser will not deliver the `keyup` events for keys
615
+ * that are currently held, which would otherwise leave stale entries in `heldKeys` and
616
+ * `keyDownTimestamps` (making a key look permanently held) and orphan any pending releases. Treat
617
+ * the blur as a release of every held key: resolve each pending release with
618
+ * `rt_key_duration: null`, since the true hold duration cannot be measured once the release is
619
+ * never observed, then clear the tracked key state.
620
+ */
621
+ rootBlurListener() {
622
+ for (const [listener, pending] of this.pendingReleases) {
623
+ this.pendingReleases.delete(listener);
624
+ pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration: null });
625
+ }
626
+ this.heldKeys.clear();
627
+ this.keyDownTimestamps.clear();
628
+ }
588
629
  isResponseValid(validResponses, allowHeldKey, key) {
589
630
  if (!allowHeldKey && this.heldKeys.has(key)) {
590
631
  return false;
@@ -605,7 +646,8 @@ class KeyboardListenerAPI {
605
646
  audio_context,
606
647
  audio_context_start_time,
607
648
  allow_held_key = false,
608
- minimum_valid_rt = this.minimumValidRt
649
+ minimum_valid_rt = this.minimumValidRt,
650
+ wait_for_key_release = false
609
651
  }) {
610
652
  if (rt_method !== "performance" && rt_method !== "audio") {
611
653
  console.log(
@@ -632,7 +674,16 @@ class KeyboardListenerAPI {
632
674
  if (!persist) {
633
675
  this.cancelKeyboardResponse(listener);
634
676
  }
635
- callback_function({ key: e.key, rt });
677
+ if (wait_for_key_release) {
678
+ this.pendingReleases.set(listener, {
679
+ code: this.getPhysicalKey(e),
680
+ key: e.key,
681
+ rt,
682
+ callback_function
683
+ });
684
+ } else {
685
+ callback_function({ key: e.key, rt });
686
+ }
636
687
  }
637
688
  };
638
689
  this.listeners.add(listener);
@@ -640,9 +691,11 @@ class KeyboardListenerAPI {
640
691
  }
641
692
  cancelKeyboardResponse(listener) {
642
693
  this.listeners.delete(listener);
694
+ this.pendingReleases.delete(listener);
643
695
  }
644
696
  cancelAllKeyboardResponses() {
645
697
  this.listeners.clear();
698
+ this.pendingReleases.clear();
646
699
  }
647
700
  compareKeys(key1, key2) {
648
701
  if (typeof key1 !== "string" && key1 !== null || typeof key2 !== "string" && key2 !== null) {