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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ![jspsych logo](http://www.jspsych.org/7.0/img/jspsych-logo.jpg)
2
2
 
3
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9910/badge)](https://www.bestpractices.dev/projects/9910)
4
+
3
5
  jsPsych is a JavaScript framework for creating behavioral experiments that run in a web browser.
4
6
 
5
7
  Experiments in jsPsych are created using [plugins](https://www.jspsych.org/latest/overview/plugins).
@@ -65,4 +67,10 @@ The project is currently managed by the core team of Josh de Leeuw ([@jodeleeuw]
65
67
 
66
68
  jsPsych was created by [Josh de Leeuw](https://www.vassar.edu/faculty/jdeleeuw).
67
69
 
68
- We're also grateful for the generous support from a [Mozilla Open Source Support award](https://www.mozilla.org/en-US/moss/), which funded development of the library from 2020-2022.
70
+ ## Thanks
71
+
72
+ We're grateful for the generous support from a [Mozilla Open Source Support award](https://www.mozilla.org/en-US/moss/), which funded development of the library from 2020-2022, and from a [National Science Foundation Grant](https://www.nsf.gov/awardsearch/show-award?AWD_ID=2346214), which funded work on the broader ecosystem from 2024-2026.
73
+
74
+ This project is tested with [BrowserStack](https://www.browserstack.com).
75
+
76
+
package/css/jspsych.css CHANGED
@@ -408,6 +408,26 @@
408
408
  cursor: not-allowed;
409
409
  }
410
410
 
411
+ /* Keyboard keys, e.g., press <kbd>space</kbd> in instructions */
412
+ .jspsych-display-element kbd,
413
+ .jspsych-display-element .jspsych-key {
414
+ display: inline-block;
415
+ min-width: 1em;
416
+ padding: 0.15em 0.5em;
417
+ margin: 0 0.1em 2px;
418
+ font-family: "Open Sans", "Arial", sans-serif;
419
+ font-size: 0.8em;
420
+ line-height: 1.4;
421
+ text-align: center;
422
+ white-space: nowrap;
423
+ vertical-align: middle;
424
+ color: #333;
425
+ background-color: #f7f7f7;
426
+ border: 1px solid #ccc;
427
+ border-radius: 4px;
428
+ box-shadow: 0 2px 0 #ccc;
429
+ }
430
+
411
431
  /* custom style for input[type="range] (slider) to improve alignment between positions and labels */
412
432
  .jspsych-slider {
413
433
  appearance: none;
@@ -51,7 +51,7 @@ var jsPsychModule = (function (exports) {
51
51
 
52
52
  var autoBind$1 = /*@__PURE__*/getDefaultExportFromCjs(autoBind);
53
53
 
54
- var version = "8.2.3";
54
+ var version = "8.3.0";
55
55
 
56
56
  class ExtensionManager {
57
57
  constructor(dependencies, extensionsConfiguration) {
@@ -604,6 +604,8 @@ var jsPsychModule = (function (exports) {
604
604
  this.minimumValidRt = minimumValidRt;
605
605
  this.listeners = /* @__PURE__ */ new Set();
606
606
  this.heldKeys = /* @__PURE__ */ new Set();
607
+ this.keyDownTimestamps = /* @__PURE__ */ new Map();
608
+ this.pendingReleases = /* @__PURE__ */ new Map();
607
609
  this.areRootListenersRegistered = false;
608
610
  autoBind$1(this);
609
611
  this.registerRootListeners();
@@ -618,11 +620,16 @@ var jsPsychModule = (function (exports) {
618
620
  if (rootElement) {
619
621
  rootElement.addEventListener("keydown", this.rootKeydownListener);
620
622
  rootElement.addEventListener("keyup", this.rootKeyupListener);
623
+ window.addEventListener("blur", this.rootBlurListener);
621
624
  this.areRootListenersRegistered = true;
622
625
  }
623
626
  }
624
627
  }
625
628
  rootKeydownListener(e) {
629
+ const physicalKey = this.getPhysicalKey(e);
630
+ if (!this.keyDownTimestamps.has(physicalKey)) {
631
+ this.keyDownTimestamps.set(physicalKey, performance.now());
632
+ }
626
633
  for (const listener of [...this.listeners]) {
627
634
  listener(e);
628
635
  }
@@ -631,9 +638,43 @@ var jsPsychModule = (function (exports) {
631
638
  toLowerCaseIfInsensitive(string) {
632
639
  return this.areResponsesCaseSensitive ? string : string.toLowerCase();
633
640
  }
641
+ /**
642
+ * identifies physical key of a keyboard event, for matching a keydown event with
643
+ * its corresponding keyup event, in the case of a change of shift state while
644
+ * the key is being held.
645
+ */
646
+ getPhysicalKey(e) {
647
+ return e.code || this.toLowerCaseIfInsensitive(e.key);
648
+ }
634
649
  rootKeyupListener(e) {
650
+ const physicalKey = this.getPhysicalKey(e);
651
+ for (const [listener, pending] of this.pendingReleases) {
652
+ if (pending.code === physicalKey) {
653
+ const pressTimestamp = this.keyDownTimestamps.get(physicalKey);
654
+ const rt_key_duration = pressTimestamp === void 0 ? null : Math.round(performance.now() - pressTimestamp);
655
+ this.pendingReleases.delete(listener);
656
+ pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration });
657
+ }
658
+ }
659
+ this.keyDownTimestamps.delete(physicalKey);
635
660
  this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));
636
661
  }
662
+ /**
663
+ * When the window loses focus (`blur`), the browser will not deliver the `keyup` events for keys
664
+ * that are currently held, which would otherwise leave stale entries in `heldKeys` and
665
+ * `keyDownTimestamps` (making a key look permanently held) and orphan any pending releases. Treat
666
+ * the blur as a release of every held key: resolve each pending release with
667
+ * `rt_key_duration: null`, since the true hold duration cannot be measured once the release is
668
+ * never observed, then clear the tracked key state.
669
+ */
670
+ rootBlurListener() {
671
+ for (const [listener, pending] of this.pendingReleases) {
672
+ this.pendingReleases.delete(listener);
673
+ pending.callback_function({ key: pending.key, rt: pending.rt, rt_key_duration: null });
674
+ }
675
+ this.heldKeys.clear();
676
+ this.keyDownTimestamps.clear();
677
+ }
637
678
  isResponseValid(validResponses, allowHeldKey, key) {
638
679
  if (!allowHeldKey && this.heldKeys.has(key)) {
639
680
  return false;
@@ -654,7 +695,8 @@ var jsPsychModule = (function (exports) {
654
695
  audio_context,
655
696
  audio_context_start_time,
656
697
  allow_held_key = false,
657
- minimum_valid_rt = this.minimumValidRt
698
+ minimum_valid_rt = this.minimumValidRt,
699
+ wait_for_key_release = false
658
700
  }) {
659
701
  if (rt_method !== "performance" && rt_method !== "audio") {
660
702
  console.log(
@@ -681,7 +723,16 @@ var jsPsychModule = (function (exports) {
681
723
  if (!persist) {
682
724
  this.cancelKeyboardResponse(listener);
683
725
  }
684
- callback_function({ key: e.key, rt });
726
+ if (wait_for_key_release) {
727
+ this.pendingReleases.set(listener, {
728
+ code: this.getPhysicalKey(e),
729
+ key: e.key,
730
+ rt,
731
+ callback_function
732
+ });
733
+ } else {
734
+ callback_function({ key: e.key, rt });
735
+ }
685
736
  }
686
737
  };
687
738
  this.listeners.add(listener);
@@ -689,9 +740,11 @@ var jsPsychModule = (function (exports) {
689
740
  }
690
741
  cancelKeyboardResponse(listener) {
691
742
  this.listeners.delete(listener);
743
+ this.pendingReleases.delete(listener);
692
744
  }
693
745
  cancelAllKeyboardResponses() {
694
746
  this.listeners.clear();
747
+ this.pendingReleases.clear();
695
748
  }
696
749
  compareKeys(key1, key2) {
697
750
  if (typeof key1 !== "string" && key1 !== null || typeof key2 !== "string" && key2 !== null) {
@@ -4143,4 +4196,4 @@ var jsPsychModule = (function (exports) {
4143
4196
 
4144
4197
  })({});
4145
4198
  var initJsPsych = jsPsychModule.initJsPsych;
4146
- //# sourceMappingURL=https://unpkg.com/jspsych@8.2.3/dist/index.browser.js.map
4199
+ //# sourceMappingURL=https://unpkg.com/jspsych@8.3.0/dist/index.browser.js.map