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.