PageSourceSearch

https://sol.vin/js/hold_click.js

js sol.vin collected 2026-10-09 21:58:25 UTC 7,066 bytes, 218 lines download raw bytes

1/**
2 * HoldClick - Reusable Left/Right Click & Hold Handler with Progress Spinner
3 */
4(function(window) {
5  'use strict';
6
7  function HoldClick(target, options) {
8    if (!(this instanceof HoldClick)) {
9      return new HoldClick(target, options);
10    }
11
12    this.container = typeof target === 'string' ? document.querySelector(target) : target;
13    if (!this.container) return;
14
15    this.options = Object.assign({
16      holdDuration: 2000,     // Total hold time (ms) = 500ms delay + 1500ms charge up
17      spinnerDelay: 500,      // Delay before showing spinner (ms)
18      delegateSelector: null, // Optional selector for event delegation
19      onLeftClick: null,      // Callback(e, targetEl)
20      onRightClick: null,     // Callback(e, targetEl)
21      onLeftHold: null,       // Callback(e, targetEl)
22      onRightHold: null       // Callback(e, targetEl)
23    }, options || {});
24
25    this.animFrameId = null;
26    this.heldLong = false;
27    this.mouseX = 0;
28    this.mouseY = 0;
29    this.spinnerEl = null;
30    this.progressCircle = null;
31    this.circumference = 2 * Math.PI * 11; // ~69.115
32
33    this._onMouseMove = this._onMouseMove.bind(this);
34    this._onMouseDown = this._onMouseDown.bind(this);
35    this._onMouseUp = this._onMouseUp.bind(this);
36    this._onMouseLeave = this._onMouseLeave.bind(this);
37    this._onClick = this._onClick.bind(this);
38    this._onContextMenu = this._onContextMenu.bind(this);
39
40    this.init();
41  }
42
43  HoldClick.prototype.ensureSpinner = function() {
44    if (this.spinnerEl) return;
45    var existing = document.getElementById('palette-hold-spinner');
46    if (existing) {
47      this.spinnerEl = existing;
48    } else {
49      this.spinnerEl = document.createElement('div');
50      this.spinnerEl.id = 'palette-hold-spinner';
51      this.spinnerEl.className = 'palette-hold-spinner hold-click-s
51pinner';
52      this.spinnerEl.innerHTML = '<svg width="32" height="32" viewBox="0 0 32 32">' +
53                                 '<circle cx="16" cy="16" r="14" class="spinner-base" />' +
54                                 '<circle cx="16" cy="16" r="11" class="spinner-track" />' +
55                                 '<circle cx="16" cy="16" r="11" class="spinner-progress" />' +
56                                 '<circle cx="16" cy="16" r="3.5" class="spinner-dot" />' +
57                                 '</svg>';
58      document.body.appendChild(this.spinnerEl);
59    }
60    this.progressCircle = this.spinnerEl.querySelector('.spinner-progress');
61  };
62
63  HoldClick.prototype._updateSpinnerPos = function(e) {
64    if (e) {
65      this.mouseX = e.clientX;
66      this.mouseY = e.clientY;
67    }
68    if (this.spinnerEl) {
69      this.spinnerEl.style.left = (this.mouseX + 14) + 'px';
70      this.spinnerEl.style.top = (this.mouseY + 14) + 'px';
71    }
72  };
73
74  HoldClick.prototype._onMouseMove = function(e) {
75    this._updateSpinnerPos(e);
76  };
77
78  HoldClick.prototype.stopHoldAnimation = function() {
79    if (this.animFrameId) {
80      cancelAnimationFrame(this.animFrameId);
81      this.animFrameId = null;
82    }
83    if (this.spinnerEl) {
84      this.spinnerEl.classList.remove('is-visible');
85    }
86    window.removeEventListener('mousemove', this._onMouseMove);
87  };
88
89  HoldClick.prototype.getTargetElement = function(e) {
90    if (this.options.delegateSelector) {
91      return e.target.closest(this.options.delegateSelector);
92    }
93    return this.container.contains(e.target) ? this.container : null;
94  };
95
96  HoldClick.prototype._onMouseDown = function(e) {
97    var targetEl = this.getTargetElement(e);
98    if (!targetEl) return;
99
100    if (e.button !== 0 && e.button !== 2) return;
101
102    this.stopHoldAnimation();
103    this.ensureSpinner();
104    this.heldLong = false;
105
106    this._updateSpinnerPos(e);
107    window.addEventListener('mousemove', this._onMouseMove);
108
109    var self = this;
110    var startTime = Date.now();
111    var isLeftClick = (e.button === 0);
112    var holdDuration = this.options.holdDuration;
113    var spinnerDelay = this.options.spinnerDelay;
114
115    function step() {
116      var elapsed = Date.now() - startTime;
117      if (elapsed >= spinnerDelay) {
118        if (self.spinnerEl) {
119          self.spinnerEl.classList.add('is-visible');
120        }
121        var progress = Math.min(1, Math.max(0, (elapsed - spinnerDelay) / (holdDuration - spinnerDelay)));
122        if (self.progressCircle) {
123          var offset = self.circumference * (1 - progress);
124          self.progressCircle.style.strokeDashoffset = offset + 'px';
125          self.progressCircle.setAttribute('stroke-dashoffset', offset);
126        }
127      } else {
128        if (self.spinnerEl) {
129          self.spinnerEl.classList.remove('is-visible');
130        }
131      }
132
133      if (elapsed >= holdDuration) {
134        self.heldLong = true;
135        self.stopHoldAnimation();
136
137        if (isLeftClick) {
138          if (typeof self.options.onLeftHold === 'function') {
139            self.options.onLeftHold(e, targetEl);
140          }
141        } else {
142          if (typeof self.options.onRightHold === 'function') {
143            self.options.onRightHold(e, targetEl);
144          }
145        }
146      } else {
147        self.animFrameId = requestAnimationFrame(step);
148      }
149    }
150
151    this.animFrameId = requestAnimationFrame(step);
152  };
153
154  HoldClick.prototype._onMouseUp = function(e) {
155    this.stopHoldAnimation();
156  };
157
158  HoldClick.prototype._onMouseLeave = function(e) {
159    this.stopHoldAnimation();
160  };
161
162  HoldClick.prototype._onClick = function(e) {
163    var targetEl = this.getTargetElement(e);
164    if (!targetEl) return;
165
166    e.preventDefault();
167    this.stopHoldAnimation();
168
169    if (this.heldLong) {
170      this.heldLong = false;
171      return;
172    }
173
174    if (typeof this.options.onLeftClick === 'function') {
175      this.options.onLeftClick(e, targetEl);
176    }
177  };
178
179  HoldClick.prototype._onContextMenu = function(e) {
180    var targetEl = this.getTargetElement(e);
181    if (!targetEl) return;
182
183    e.preventDefault();
184    this.stopHoldAnimation();
185
186    if (this.heldLong) {
187      this.heldLong = false;
188      return;
189    }
190
191    if (typeof this.options.onRightClick === 'function') {
192      this.options.onRightClick(e, targetEl);
193    }
194  };
195
196  HoldClick.prototype.init = function() {
197    this.container.addEventListener('mousedown', this._onMouseDown);
198    this.container.addEventListener('mouseup', this._onMouseUp);
199    this.container.addEventListener('mouseleave', this._onMouseLeave);
200    this.container.addEventListener('click', this._onClick);
201    this.container.addEventListener('contextmenu', this._onContextMenu);
202  };
203
204  HoldClick.prototype.destroy = function() {
205    this.stopHoldAnimation();
206    this.container.removeEventListener('mousedown', this._onMouseDown);
207    this.container.removeEventListener('mouseup', this._onMouseUp);
208    this.container.removeEventListener('mouseleave', this._onMouseLeave);
209    this.container.removeEventListener('click', this._onClick);
210    this.container.removeEventListener('contextmenu', this._onContextMenu);
211  };
212
213  HoldClick.attach = function(target, options) {
214    return new HoldClick(target, options);
215  };
216
217  window.HoldClick = HoldClick;
218})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.