@kieransimkin/dancemoves 3.1.17 → 3.1.18

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/lib/index.mjs CHANGED
@@ -2300,7 +2300,10 @@ function install2(__dmScope) {
2300
2300
  function tick(now) {
2301
2301
  if (!running)
2302
2302
  return;
2303
- if (document2.hidden || state.reduced.matches || state.forced.matches) {
2303
+ var preferenceBlocked = state.reduced.matches || state.forced.matches;
2304
+ if (preferenceBlocked !== preferenceLimited)
2305
+ preference();
2306
+ if (document2.hidden || preferenceBlocked) {
2304
2307
  resetWindow(now);
2305
2308
  frame = window2.requestAnimationFrame(tick);
2306
2309
  return;
@@ -3190,6 +3193,7 @@ function install5(__dmScope) {
3190
3193
  if (!Core || !harnessMode && (!Core.isMobileDevice(window) || !Core.sensorSupported(window)))
3191
3194
  return;
3192
3195
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
3196
+ const forcedColours = window.matchMedia("(forced-colors: active)");
3193
3197
  const perceptualAxis = (value) => {
3194
3198
  const bounded = Core.clamp(value, -1, 1);
3195
3199
  return Math.sign(bounded) * Math.sqrt(Math.abs(bounded));
@@ -3518,6 +3522,10 @@ function install5(__dmScope) {
3518
3522
  let destroyed = false;
3519
3523
  let controlFeedback = null;
3520
3524
  const phonePreference = Core.createOrientationPreference(window);
3525
+ const subscribers = /* @__PURE__ */ new Set();
3526
+ let suspended = false;
3527
+ const qualityLimited = () => ["minimal", "static"].includes(adapter.root.dataset.danceMovesQuality) || adapter.root.dataset.danceMovesPerformance === "minimal" || document.body?.dataset?.epkAdaptiveTier === "static";
3528
+ const notify = (reason) => subscribers.forEach((callback) => callback({ ...latest, reason }));
3521
3529
  __dmScope.dataset(adapter.root, "ksOrientation", "supported");
3522
3530
  __dmScope.dataset(adapter.root, "ksOrientationAdapter", config.adapter || "detected");
3523
3531
  __dmScope.dataset(adapter.root, "ksOrientationWindow", String(windowMilliseconds));
@@ -3549,6 +3557,7 @@ function install5(__dmScope) {
3549
3557
  removeControl();
3550
3558
  }
3551
3559
  adapter.apply(x, y);
3560
+ notify("phone");
3552
3561
  window.EPKEffectHarnessProbe?.noteCommit?.(detail.sampleAge);
3553
3562
  }
3554
3563
  });
@@ -3572,9 +3581,10 @@ function install5(__dmScope) {
3572
3581
  active = false;
3573
3582
  __dmScope.dataset(adapter.root, "ksOrientation", "supported");
3574
3583
  adapter.reset();
3584
+ notify("reset");
3575
3585
  };
3576
3586
  const onOrientation = (event) => {
3577
- if (reducedMotion.matches)
3587
+ if (reducedMotion.matches || forcedColours.matches || suspended || qualityLimited())
3578
3588
  return;
3579
3589
  if (!Core.hasMotionData(event)) {
3580
3590
  window.EPKEffectHarnessProbe?.noteRejected?.();
@@ -3583,7 +3593,7 @@ function install5(__dmScope) {
3583
3593
  scheduler.receive(event, Core.screenAngle(window), window.performance.now());
3584
3594
  };
3585
3595
  const startListening = () => {
3586
- if (listening || reducedMotion.matches || document.hidden || destroyed)
3596
+ if (listening || reducedMotion.matches || forcedColours.matches || suspended || qualityLimited() || document.hidden || destroyed)
3587
3597
  return;
3588
3598
  listening = true;
3589
3599
  __dmScope.listen(window, "deviceorientation", onOrientation, { passive: true });
@@ -3602,7 +3612,7 @@ function install5(__dmScope) {
3602
3612
  reset();
3603
3613
  };
3604
3614
  const makePermissionControl = () => {
3605
- if (control || destroyed)
3615
+ if (control || destroyed || active)
3606
3616
  return;
3607
3617
  control = document.createElement("button");
3608
3618
  control.type = "button";
@@ -3621,7 +3631,7 @@ function install5(__dmScope) {
3621
3631
  button.disabled = true;
3622
3632
  button.textContent = "Checking motion\u2026";
3623
3633
  try {
3624
- const result = await window.DeviceOrientationEvent.requestPermission();
3634
+ const result = Core.permissionRequired(window) ? await window.DeviceOrientationEvent.requestPermission() : "granted";
3625
3635
  if (destroyed || control !== button)
3626
3636
  return;
3627
3637
  if (result !== "granted") {
@@ -3660,7 +3670,7 @@ function install5(__dmScope) {
3660
3670
  stopListening();
3661
3671
  };
3662
3672
  const pageShown = () => {
3663
- if (destroyed || reducedMotion.matches)
3673
+ if (destroyed || reducedMotion.matches || forcedColours.matches || suspended || qualityLimited())
3664
3674
  return;
3665
3675
  if (harnessMode)
3666
3676
  startListening();
@@ -3668,8 +3678,11 @@ function install5(__dmScope) {
3668
3678
  makePermissionControl();
3669
3679
  if (phonePreference.read() === "enabled")
3670
3680
  startListening();
3671
- } else if (phonePreference.read() !== "disabled")
3672
- startListening();
3681
+ } else {
3682
+ makePermissionControl();
3683
+ if (phonePreference.read() !== "disabled")
3684
+ startListening();
3685
+ }
3673
3686
  };
3674
3687
  __dmScope.listen(window, "orientationchange", recalibrate, { passive: true });
3675
3688
  if (window.screen && window.screen.orientation && typeof window.screen.orientation.addEventListener === "function") {
@@ -3679,7 +3692,7 @@ function install5(__dmScope) {
3679
3692
  __dmScope.listen(window, "pagehide", pageHidden);
3680
3693
  __dmScope.listen(window, "pageshow", pageShown);
3681
3694
  const preferenceChanged = () => {
3682
- if (reducedMotion.matches) {
3695
+ if (reducedMotion.matches || forcedColours.matches || suspended || qualityLimited()) {
3683
3696
  controlFeedback?.clear();
3684
3697
  stopListening();
3685
3698
  removeControl();
@@ -3691,6 +3704,10 @@ function install5(__dmScope) {
3691
3704
  __dmScope.listen(reducedMotion, "change", preferenceChanged);
3692
3705
  else
3693
3706
  __dmScope.addMediaListener(reducedMotion, preferenceChanged);
3707
+ if (typeof forcedColours.addEventListener === "function")
3708
+ __dmScope.listen(forcedColours, "change", preferenceChanged);
3709
+ else if (typeof forcedColours.addListener === "function")
3710
+ __dmScope.addMediaListener(forcedColours, preferenceChanged);
3694
3711
  const teardown = () => {
3695
3712
  if (destroyed)
3696
3713
  return;
@@ -3704,6 +3721,8 @@ function install5(__dmScope) {
3704
3721
  latest = { x: 0, y: 0 };
3705
3722
  active = false;
3706
3723
  adapter.reset();
3724
+ notify("teardown");
3725
+ subscribers.clear();
3707
3726
  controlFeedback?.destroy();
3708
3727
  removeControl();
3709
3728
  __dmScope.unlisten(window, "orientationchange", recalibrate);
@@ -3717,6 +3736,10 @@ function install5(__dmScope) {
3717
3736
  __dmScope.unlisten(reducedMotion, "change", preferenceChanged);
3718
3737
  else if (typeof reducedMotion.removeListener === "function")
3719
3738
  __dmScope.removeMediaListener(reducedMotion, preferenceChanged);
3739
+ if (typeof forcedColours.removeEventListener === "function")
3740
+ __dmScope.unlisten(forcedColours, "change", preferenceChanged);
3741
+ else if (typeof forcedColours.removeListener === "function")
3742
+ __dmScope.removeMediaListener(forcedColours, preferenceChanged);
3720
3743
  observer?.disconnect();
3721
3744
  observer = null;
3722
3745
  if (window.__ksEpkOrientationRuntime?.teardown === teardown)
@@ -3729,15 +3752,34 @@ function install5(__dmScope) {
3729
3752
  destroyed,
3730
3753
  latest: { ...latest },
3731
3754
  reducedMotion: reducedMotion.matches,
3755
+ forcedColors: forcedColours.matches,
3756
+ suspended,
3732
3757
  documentHidden: document.hidden
3733
3758
  });
3734
- window.__ksEpkOrientationRuntime = Object.freeze({ reset, teardown, snapshot });
3759
+ window.__ksEpkOrientationRuntime = Object.freeze({
3760
+ reset,
3761
+ teardown,
3762
+ snapshot,
3763
+ subscribe(callback) {
3764
+ if (typeof callback !== "function")
3765
+ throw new TypeError("Orientation callback required");
3766
+ subscribers.add(callback);
3767
+ callback({ ...latest, reason: active ? "phone" : "reset" });
3768
+ return () => subscribers.delete(callback);
3769
+ },
3770
+ suspend(value) {
3771
+ suspended = value === true;
3772
+ preferenceChanged();
3773
+ }
3774
+ });
3735
3775
  if (typeof window.MutationObserver === "function" && document.documentElement) {
3736
- observer = new window.MutationObserver(() => {
3776
+ observer = new window.MutationObserver((records) => {
3737
3777
  if (adapter.root.isConnected === false)
3738
3778
  teardown();
3779
+ else if (records?.some((record) => record.type === "attributes"))
3780
+ preferenceChanged();
3739
3781
  });
3740
- observer.observe(document.documentElement, { childList: true, subtree: true });
3782
+ observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-dance-moves-quality", "data-dance-moves-performance", "data-epk-adaptive-tier"] });
3741
3783
  }
3742
3784
  pageShown();
3743
3785
  }
@@ -5497,7 +5539,7 @@ function createOrientationController(scope, { render, windowMilliseconds = 2e3,
5497
5539
  for (const [name, value] of Object.entries({ windowMilliseconds, minimumSpanDegrees, smoothingTimeConstantMilliseconds })) if (!Number.isFinite(value) || value <= 0) throw new RangeError(name);
5498
5540
  const w = scope.window, core = w.KSEpkOrientationCore;
5499
5541
  const preference = core.createOrientationPreference(w);
5500
- let enabled = core.sensorSupported(w) && preference.read() === "enabled", listening = false, destroyed = false, pending = false;
5542
+ let enabled = core.sensorSupported(w) && preference.read() === "enabled", listening = false, destroyed = false, pending = false, active = true, pageHidden = false;
5501
5543
  const reduced = w.matchMedia("(prefers-reduced-motion: reduce)"), forced = w.matchMedia("(forced-colors: active)");
5502
5544
  const scheduler = core.createLatestSampleRafScheduler({
5503
5545
  requestFrame: w.requestAnimationFrame,
@@ -5515,27 +5557,49 @@ function createOrientationController(scope, { render, windowMilliseconds = 2e3,
5515
5557
  if (!destroyed) render({ x: 0, y: 0, reason: "reset" });
5516
5558
  };
5517
5559
  const reconcile = () => {
5518
- const active = enabled && !destroyed && !scope.disposed && !scope.document.hidden && !reduced.matches && !forced.matches;
5519
- if (active && !listening) {
5560
+ const allowed = active && enabled && !destroyed && !scope.disposed && !pageHidden && !scope.document.hidden && !reduced.matches && !forced.matches;
5561
+ if (allowed && !listening) {
5520
5562
  scope.listen(w, "deviceorientation", receive, { passive: true });
5521
5563
  listening = true;
5522
5564
  }
5523
- if (!active && listening) {
5565
+ if (!allowed && listening) {
5524
5566
  scope.unlisten(w, "deviceorientation", receive);
5525
5567
  listening = false;
5526
5568
  reset();
5527
5569
  }
5528
5570
  };
5571
+ const hide = () => {
5572
+ pageHidden = true;
5573
+ reconcile();
5574
+ };
5575
+ const show = () => {
5576
+ pageHidden = false;
5577
+ reset();
5578
+ reconcile();
5579
+ };
5529
5580
  const disposers = [
5530
5581
  () => scope.unlisten(scope.document, "visibilitychange", reconcile),
5531
5582
  () => scope.unlisten(reduced, "change", reconcile),
5532
5583
  () => scope.unlisten(forced, "change", reconcile),
5533
- () => scope.unlisten(w, "orientationchange", reset)
5584
+ () => scope.unlisten(w, "orientationchange", reset),
5585
+ () => scope.unlisten(w.screen?.orientation, "change", reset),
5586
+ () => scope.unlisten(w, "pagehide", hide),
5587
+ () => scope.unlisten(w, "pageshow", show)
5534
5588
  ];
5535
5589
  scope.listen(scope.document, "visibilitychange", reconcile);
5536
5590
  scope.listen(reduced, "change", reconcile);
5537
5591
  scope.listen(forced, "change", reconcile);
5538
5592
  scope.listen(w, "orientationchange", reset);
5593
+ scope.listen(w.screen?.orientation, "change", reset);
5594
+ scope.listen(w, "pagehide", hide);
5595
+ scope.listen(w, "pageshow", show);
5596
+ let observer;
5597
+ if (w.MutationObserver) {
5598
+ observer = new w.MutationObserver(() => {
5599
+ if (w.DanceMoves?.root?.isConnected === false) destroy();
5600
+ });
5601
+ observer.observe(w.DanceMoves?.root?.ownerDocument.documentElement || scope.document.documentElement, { childList: true, subtree: true });
5602
+ }
5539
5603
  function destroy() {
5540
5604
  if (destroyed) return;
5541
5605
  enabled = false;
@@ -5543,6 +5607,7 @@ function createOrientationController(scope, { render, windowMilliseconds = 2e3,
5543
5607
  destroyed = true;
5544
5608
  pending = false;
5545
5609
  scheduler.teardown();
5610
+ observer?.disconnect();
5546
5611
  disposers.forEach((fn) => fn());
5547
5612
  removeDispose();
5548
5613
  }
@@ -5555,6 +5620,7 @@ function createOrientationController(scope, { render, windowMilliseconds = 2e3,
5555
5620
  if (!core.sensorSupported(w)) throw new Error("Device orientation is unavailable in this browser or insecure context");
5556
5621
  pending = true;
5557
5622
  try {
5623
+ if (!active || pageHidden || scope.document.hidden || reduced.matches || forced.matches) throw new Error("Phone motion is currently paused");
5558
5624
  if (core.permissionRequired(w) && await w.DeviceOrientationEvent.requestPermission() !== "granted") {
5559
5625
  if (!destroyed && !scope.disposed) {
5560
5626
  preference.write(false);
@@ -5577,12 +5643,175 @@ function createOrientationController(scope, { render, windowMilliseconds = 2e3,
5577
5643
  enabled = false;
5578
5644
  reconcile();
5579
5645
  },
5646
+ setActive(value) {
5647
+ active = value === true;
5648
+ reconcile();
5649
+ },
5580
5650
  reset,
5581
5651
  destroy,
5582
- snapshot: () => ({ enabled, listening, destroyed, pending, reducedMotion: reduced.matches, forcedColors: forced.matches, framePending: scheduler.pending() })
5652
+ snapshot: () => ({ enabled, listening, destroyed, pending, active, pageHidden, reducedMotion: reduced.matches, forcedColors: forced.matches, framePending: scheduler.pending() })
5583
5653
  });
5584
5654
  }
5585
5655
 
5656
+ // .build/modules/phone-motion.mjs
5657
+ var connections = /* @__PURE__ */ new WeakMap();
5658
+ function connectPhoneMotion(scope, { render = () => {
5659
+ }, pointers = [], enabled = () => true, controlParent = scope.document.body, watchAttributes = ["class"], windowMilliseconds = 1e4 } = {}) {
5660
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
5661
+ if (connections.has(scope)) throw new Error("Phone motion is already connected on this mount");
5662
+ if (typeof render !== "function" || typeof enabled !== "function") throw new TypeError("Phone motion callbacks must be functions");
5663
+ if (!Array.isArray(pointers) || !Array.isArray(watchAttributes)) throw new TypeError("Phone motion lists must be arrays");
5664
+ const w = scope.window, core = w.KSEpkOrientationCore;
5665
+ let destroyed = false, sensorActive = false, pageHidden = false, button = null, timer = 0, pending = false, observer = null, source = null, unsubscribe = null;
5666
+ const reduced = w.matchMedia("(prefers-reduced-motion: reduce)"), forced = w.matchMedia("(forced-colors: active)");
5667
+ const eligible = () => core.isMobileDevice(w) && core.sensorSupported(w);
5668
+ if (!eligible()) {
5669
+ const dispose = () => {
5670
+ destroyed = true;
5671
+ connections.delete(scope);
5672
+ };
5673
+ scope.onDispose(dispose);
5674
+ const unavailable2 = Object.freeze({ enable: async () => false, reconcile: () => {
5675
+ }, destroy: dispose, snapshot: () => ({ eligible: false, allowed: false, sensorActive: false, destroyed: destroyed || scope.disposed, source: "unavailable", orientation: null }) });
5676
+ connections.set(scope, unavailable2);
5677
+ return unavailable2;
5678
+ }
5679
+ const allowed = () => eligible() && !destroyed && !scope.disposed && !pageHidden && !scope.document.hidden && !reduced.matches && !forced.matches && enabled();
5680
+ function apply(point) {
5681
+ const live = allowed() && point.reason !== "reset" && point.reason !== "teardown";
5682
+ const x = live ? core.clamp(point.x, -1, 1) : 0, y = live ? core.clamp(point.y, -1, 1) : 0;
5683
+ for (const id of pointers) {
5684
+ const pointer = w.DanceMovesEffects.get(id);
5685
+ if (pointer) {
5686
+ live ? pointer.set(x, y, "phone") : pointer.reset("phone-reset");
5687
+ }
5688
+ }
5689
+ render({ x, y, reason: live ? "phone" : "reset" });
5690
+ if (live) {
5691
+ sensorActive = true;
5692
+ removeButton();
5693
+ } else sensorActive = false;
5694
+ }
5695
+ function removeButton() {
5696
+ if (timer) w.clearTimeout(timer);
5697
+ timer = 0;
5698
+ if (button) {
5699
+ scope.unlisten(button, "click", enable);
5700
+ button.remove();
5701
+ button = null;
5702
+ }
5703
+ }
5704
+ function unavailable() {
5705
+ timer = 0;
5706
+ if (!button || sensorActive) return;
5707
+ button.disabled = false;
5708
+ button.textContent = core.permissionRequired(w) ? "Use phone motion" : "Phone motion unavailable";
5709
+ }
5710
+ function makeButton() {
5711
+ if (button || !allowed() || source?.subscribe) return;
5712
+ button = scope.document.createElement("button");
5713
+ button.type = "button";
5714
+ button.className = "ks-epk-orientation-control";
5715
+ button.textContent = "Use phone motion";
5716
+ button.setAttribute("aria-live", "polite");
5717
+ scope.listen(button, "click", enable);
5718
+ controlParent.append(button);
5719
+ }
5720
+ async function enable() {
5721
+ if (!allowed() || pending || !source) return false;
5722
+ pending = true;
5723
+ makeButton();
5724
+ if (button) {
5725
+ button.disabled = true;
5726
+ button.textContent = "Checking motion\u2026";
5727
+ }
5728
+ try {
5729
+ await source.enable();
5730
+ if (destroyed) return false;
5731
+ if (button) {
5732
+ button.textContent = "Move phone to enable\u2026";
5733
+ button.disabled = false;
5734
+ }
5735
+ if (timer) w.clearTimeout(timer);
5736
+ timer = w.setTimeout(unavailable, 3500);
5737
+ return true;
5738
+ } catch (error) {
5739
+ if (button) {
5740
+ button.disabled = false;
5741
+ button.textContent = /denied/i.test(error.message) ? "Phone motion blocked" : "Use phone motion";
5742
+ }
5743
+ return false;
5744
+ } finally {
5745
+ pending = false;
5746
+ }
5747
+ }
5748
+ function reconcile() {
5749
+ if (destroyed) return;
5750
+ if (source?.subscribe) {
5751
+ source.suspend?.(!allowed());
5752
+ if (!allowed()) apply({ x: 0, y: 0, reason: "reset" });
5753
+ return;
5754
+ }
5755
+ source?.setActive(allowed());
5756
+ if (!allowed()) {
5757
+ removeButton();
5758
+ apply({ x: 0, y: 0, reason: "reset" });
5759
+ return;
5760
+ }
5761
+ if (!sensorActive) makeButton();
5762
+ const state = source?.snapshot();
5763
+ if (state?.listening && !timer && !sensorActive) timer = w.setTimeout(unavailable, 3500);
5764
+ if (source && !core.permissionRequired(w) && core.createOrientationPreference(w).read() !== "disabled" && !state.enabled) void enable();
5765
+ }
5766
+ const hide = () => {
5767
+ pageHidden = true;
5768
+ reconcile();
5769
+ };
5770
+ const show = () => {
5771
+ pageHidden = false;
5772
+ reconcile();
5773
+ };
5774
+ const signals = [[scope.document, "visibilitychange", reconcile], [reduced, "change", reconcile], [forced, "change", reconcile], [w, "pagehide", hide], [w, "pageshow", show]];
5775
+ signals.forEach(([target, type, handler]) => scope.listen(target, type, handler));
5776
+ if (eligible()) {
5777
+ const existing = w.DanceMoves?.getOrientation?.();
5778
+ if (existing?.subscribe) {
5779
+ source = existing;
5780
+ unsubscribe = existing.subscribe(apply);
5781
+ } else source = createOrientationController(scope, { render: apply, windowMilliseconds });
5782
+ }
5783
+ if (w.MutationObserver) {
5784
+ const root = w.DanceMoves.root, document = root.ownerDocument;
5785
+ observer = new w.MutationObserver((records) => {
5786
+ if (root.isConnected === false) {
5787
+ destroy();
5788
+ return;
5789
+ }
5790
+ if (records.some((r) => r.type === "attributes")) reconcile();
5791
+ });
5792
+ observer.observe(root, { attributes: true, attributeFilter: watchAttributes, childList: true, subtree: true });
5793
+ if (document.body !== root) observer.observe(document.body, { attributes: true, attributeFilter: watchAttributes });
5794
+ if (document.documentElement !== root) observer.observe(document.documentElement, { childList: true, subtree: true });
5795
+ }
5796
+ function destroy() {
5797
+ if (destroyed) return;
5798
+ removeButton();
5799
+ apply({ x: 0, y: 0, reason: "teardown" });
5800
+ destroyed = true;
5801
+ unsubscribe?.();
5802
+ if (!source?.subscribe) source?.destroy();
5803
+ observer?.disconnect();
5804
+ signals.forEach(([target, type, handler]) => scope.unlisten(target, type, handler));
5805
+ removeDispose();
5806
+ connections.delete(scope);
5807
+ }
5808
+ const removeDispose = scope.onDispose(destroy);
5809
+ reconcile();
5810
+ const connection = Object.freeze({ enable, reconcile, destroy, snapshot: () => ({ eligible: eligible(), allowed: allowed(), sensorActive, destroyed, pending, source: source?.subscribe ? "existing" : "generic", orientation: source?.snapshot() || null }) });
5811
+ connections.set(scope, connection);
5812
+ return connection;
5813
+ }
5814
+
5586
5815
  // .build/modules/recorder.mjs
5587
5816
  function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples = 500 } = {}) {
5588
5817
  if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
@@ -5690,7 +5919,7 @@ function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples =
5690
5919
  }
5691
5920
 
5692
5921
  // .build/modules/version.mjs
5693
- var VERSION = "3.1.17";
5922
+ var VERSION = "3.1.18";
5694
5923
 
5695
5924
  // .build/modules/index.mjs
5696
5925
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -5749,6 +5978,7 @@ function createDanceMoves(options = {}) {
5749
5978
  createRecorder: (settings) => createMotionRecorder(scope, settings),
5750
5979
  createOrientation: (settings) => createOrientationController(scope, settings),
5751
5980
  destroy,
5981
+ connectPhoneMotion: (settings) => connectPhoneMotion(scope, settings),
5752
5982
  resources: () => scope.snapshot(),
5753
5983
  on(type, callback) {
5754
5984
  scope.listen(scope.document, type, callback);