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