PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.2.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.2.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / src / campaign-builder / components / ColorPicker.js

ColorPicker.js in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.2.1, at src/campaign-builder/components/ColorPicker.js

314 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useState, useRef, useEffect, useCallback } from '@wordpress/element';
2 import { __ } from '@wordpress/i18n';
3
4 // ── Color math helpers ────────────────────────────────────────────────────────
5
6 function clamp( v, lo, hi ) { return Math.max( lo, Math.min( hi, v ) ); }
7
8 function hsvToRgb( h, s, v ) {
9 const c = v * s;
10 const x = c * ( 1 - Math.abs( ( ( h / 60 ) % 2 ) - 1 ) );
11 const m = v - c;
12 let r = 0, g = 0, b = 0;
13 if ( h < 60 ) { r = c; g = x; }
14 else if ( h < 120 ) { r = x; g = c; }
15 else if ( h < 180 ) { g = c; b = x; }
16 else if ( h < 240 ) { g = x; b = c; }
17 else if ( h < 300 ) { r = x; b = c; }
18 else { r = c; b = x; }
19 return [
20 Math.round( ( r + m ) * 255 ),
21 Math.round( ( g + m ) * 255 ),
22 Math.round( ( b + m ) * 255 ),
23 ];
24 }
25
26 function rgbToHsv( r, g, b ) {
27 r /= 255; g /= 255; b /= 255;
28 const max = Math.max( r, g, b ), min = Math.min( r, g, b ), d = max - min;
29 let h = 0;
30 const s = max === 0 ? 0 : d / max;
31 const v = max;
32 if ( d !== 0 ) {
33 if ( max === r ) h = ( ( g - b ) / d + ( g < b ? 6 : 0 ) ) / 6;
34 else if ( max === g ) h = ( ( b - r ) / d + 2 ) / 6;
35 else h = ( ( r - g ) / d + 4 ) / 6;
36 }
37 return { h: h * 360, s, v };
38 }
39
40 function hexToRgb( hex ) {
41 const c = hex.replace( '#', '' );
42 const f = c.length === 3 ? c.split( '' ).map( x => x + x ).join( '' ) : c;
43 return [
44 parseInt( f.slice( 0, 2 ), 16 ),
45 parseInt( f.slice( 2, 4 ), 16 ),
46 parseInt( f.slice( 4, 6 ), 16 ),
47 ];
48 }
49
50 function rgbToHex( r, g, b ) {
51 return '#' + [ r, g, b ]
52 .map( v => clamp( Math.round( v ), 0, 255 ).toString( 16 ).padStart( 2, '0' ) )
53 .join( '' )
54 .toUpperCase();
55 }
56
57 function hexToHsv( hex ) {
58 try {
59 const [ r, g, b ] = hexToRgb( hex );
60 return rgbToHsv( r, g, b );
61 } catch ( e ) {
62 return { h: 0, s: 0, v: 0 };
63 }
64 }
65
66 function hsvToHex( h, s, v ) {
67 return rgbToHex( ...hsvToRgb( h, s, v ) );
68 }
69
70 function rgbToHsl( r, g, b ) {
71 r /= 255; g /= 255; b /= 255;
72 const max = Math.max( r, g, b ), min = Math.min( r, g, b );
73 const l = ( max + min ) / 2;
74 let h = 0, s = 0;
75 const d = max - min;
76 if ( d !== 0 ) {
77 s = d / ( 1 - Math.abs( 2 * l - 1 ) );
78 if ( max === r ) h = ( ( g - b ) / d + ( g < b ? 6 : 0 ) ) / 6;
79 else if ( max === g ) h = ( ( b - r ) / d + 2 ) / 6;
80 else h = ( ( r - g ) / d + 4 ) / 6;
81 }
82 return { h: Math.round( h * 360 ), s: Math.round( s * 100 ), l: Math.round( l * 100 ) };
83 }
84
85 function hslToHex( h, s, l ) {
86 s /= 100; l /= 100;
87 const c = ( 1 - Math.abs( 2 * l - 1 ) ) * s;
88 const x = c * ( 1 - Math.abs( ( h / 60 ) % 2 - 1 ) );
89 const m = l - c / 2;
90 let r = 0, g = 0, b = 0;
91 if ( h < 60 ) { r = c; g = x; }
92 else if ( h < 120 ) { r = x; g = c; }
93 else if ( h < 180 ) { g = c; b = x; }
94 else if ( h < 240 ) { g = x; b = c; }
95 else if ( h < 300 ) { r = x; b = c; }
96 else { r = c; b = x; }
97 return rgbToHex(
98 Math.round( ( r + m ) * 255 ),
99 Math.round( ( g + m ) * 255 ),
100 Math.round( ( b + m ) * 255 ),
101 );
102 }
103
104 // ── Constants ─────────────────────────────────────────────────────────────────
105
106 const PRESETS = [ '#2B66D1', '#7BB8F0', '#5AA152', '#F0C030', '#E8613A', '#D4567A' ];
107 const CANVAS_W = 260;
108 const CANVAS_H = 130;
109
110 // ── Component ─────────────────────────────────────────────────────────────────
111
112 export default function ColorPicker( { value, defaultValue, onChange, onClose } ) {
113 const safe = /^#[0-9a-fA-F]{6}$/.test( value ) ? value : ( defaultValue || '#000000' );
114 const initHsv = hexToHsv( safe );
115
116 const [ hue, setHue ] = useState( initHsv.h );
117 const [ sat, setSat ] = useState( initHsv.s );
118 const [ val, setVal ] = useState( initHsv.v );
119 const [ mode, setMode ] = useState( 'hex' );
120 const [ hexInput, setHexInput ] = useState( safe.toUpperCase() );
121
122 const canvasRef = useRef( null );
123 const dragging = useRef( false );
124
125 const currentHex = hsvToHex( hue, sat, val );
126 const [ r, g, b ] = hexToRgb( currentHex );
127 const hsl = rgbToHsl( r, g, b );
128
129 // Draw SV gradient whenever hue changes.
130 useEffect( () => {
131 const canvas = canvasRef.current;
132 if ( ! canvas ) return;
133 const ctx = canvas.getContext( '2d' );
134 const w = canvas.width, h = canvas.height;
135
136 const gH = ctx.createLinearGradient( 0, 0, w, 0 );
137 gH.addColorStop( 0, '#fff' );
138 gH.addColorStop( 1, `hsl(${ hue },100%,50%)` );
139 ctx.fillStyle = gH;
140 ctx.fillRect( 0, 0, w, h );
141
142 const gV = ctx.createLinearGradient( 0, 0, 0, h );
143 gV.addColorStop( 0, 'rgba(0,0,0,0)' );
144 gV.addColorStop( 1, '#000' );
145 ctx.fillStyle = gV;
146 ctx.fillRect( 0, 0, w, h );
147 }, [ hue ] );
148
149 const pickFromCanvas = useCallback( ( e ) => {
150 const canvas = canvasRef.current;
151 if ( ! canvas ) return;
152 const rect = canvas.getBoundingClientRect();
153 const x = clamp( ( e.clientX - rect.left ) / rect.width, 0, 1 );
154 const y = clamp( ( e.clientY - rect.top ) / rect.height, 0, 1 );
155 const ns = x, nv = 1 - y;
156 setSat( ns ); setVal( nv );
157 const hex = hsvToHex( hue, ns, nv );
158 setHexInput( hex );
159 onChange( hex );
160 }, [ hue, onChange ] );
161
162 // Global mouse-move / up for canvas drag.
163 useEffect( () => {
164 const onMove = ( e ) => { if ( dragging.current ) pickFromCanvas( e ); };
165 const onUp = () => { dragging.current = false; };
166 window.addEventListener( 'mousemove', onMove );
167 window.addEventListener( 'mouseup', onUp );
168 return () => {
169 window.removeEventListener( 'mousemove', onMove );
170 window.removeEventListener( 'mouseup', onUp );
171 };
172 }, [ pickFromCanvas ] );
173
174 // Apply a hex string and sync all state.
175 const applyHex = useCallback( ( hex ) => {
176 const clean = hex.toUpperCase();
177 const hsv = hexToHsv( clean );
178 setHue( hsv.h ); setSat( hsv.s ); setVal( hsv.v );
179 setHexInput( clean );
180 onChange( clean );
181 }, [ onChange ] );
182
183 const handleHueChange = ( e ) => {
184 const h = parseFloat( e.target.value );
185 setHue( h );
186 const hex = hsvToHex( h, sat, val );
187 setHexInput( hex );
188 onChange( hex );
189 };
190
191 const handleHexInput = ( e ) => {
192 const raw = e.target.value;
193 setHexInput( raw );
194 if ( /^#[0-9a-fA-F]{6}$/.test( raw ) ) {
195 const hsv = hexToHsv( raw );
196 setHue( hsv.h ); setSat( hsv.s ); setVal( hsv.v );
197 onChange( raw.toUpperCase() );
198 }
199 };
200
201 const handleRgbChange = ( channel ) => ( e ) => {
202 const next = { r, g, b, [ channel ]: clamp( parseInt( e.target.value, 10 ) || 0, 0, 255 ) };
203 applyHex( rgbToHex( next.r, next.g, next.b ) );
204 };
205
206 const handleHslChange = ( channel ) => ( e ) => {
207 const limits = { h: [ 0, 360 ], s: [ 0, 100 ], l: [ 0, 100 ] };
208 const [ lo, hi ] = limits[ channel ];
209 const next = { ...hsl, [ channel ]: clamp( parseInt( e.target.value, 10 ) || 0, lo, hi ) };
210 applyHex( hslToHex( next.h, next.s, next.l ) );
211 };
212
213 return (
214 <div className="bp-color-picker">
215
216 { /* Gradient canvas */ }
217 <div className="bp-color-picker__canvas-wrap">
218 <canvas
219 ref={ canvasRef }
220 width={ CANVAS_W }
221 height={ CANVAS_H }
222 className="bp-color-picker__canvas"
223 onMouseDown={ ( e ) => { dragging.current = true; pickFromCanvas( e ); } }
224 />
225 <div
226 className="bp-color-picker__cursor"
227 style={ { left: `${ sat * 100 }%`, top: `${ ( 1 - val ) * 100 }%` } }
228 />
229 </div>
230
231 { /* Hue slider */ }
232 <input
233 type="range"
234 className="bp-color-picker__hue"
235 min="0" max="360" step="1"
236 value={ Math.round( hue ) }
237 onChange={ handleHueChange }
238 />
239
240 { /* Preview + mode input */ }
241 <div className="bp-color-picker__preview-row">
242 <div className="bp-color-picker__swatch-preview" style={ { background: currentHex } } />
243
244 { mode === 'hex' && (
245 <input
246 type="text"
247 className="bp-color-picker__text-input"
248 value={ hexInput }
249 onChange={ handleHexInput }
250 onBlur={ () => setHexInput( currentHex ) }
251 maxLength={ 7 }
252 spellCheck={ false }
253 />
254 ) }
255
256 { mode === 'rgb' && (
257 <div className="bp-color-picker__channel-inputs">
258 { [ [ 'r', r ], [ 'g', g ], [ 'b', b ] ].map( ( [ ch, cv ] ) => (
259 <input key={ ch } type="number" min="0" max="255"
260 value={ cv } onChange={ handleRgbChange( ch ) } />
261 ) ) }
262 </div>
263 ) }
264
265 { mode === 'hsl' && (
266 <div className="bp-color-picker__channel-inputs">
267 { [ [ 'h', hsl.h, 360 ], [ 's', hsl.s, 100 ], [ 'l', hsl.l, 100 ] ].map( ( [ ch, cv, mx ] ) => (
268 <input key={ ch } type="number" min="0" max={ mx }
269 value={ cv } onChange={ handleHslChange( ch ) } />
270 ) ) }
271 </div>
272 ) }
273 </div>
274
275 { /* Mode tabs */ }
276 <div className="bp-color-picker__modes">
277 { [ 'hex', 'rgb', 'hsl' ].map( ( m ) => (
278 <button key={ m } type="button"
279 className={ `bp-color-picker__mode-btn${ mode === m ? ' is-active' : '' }` }
280 onClick={ () => setMode( m ) }
281 >
282 { m.toUpperCase() }
283 </button>
284 ) ) }
285 </div>
286
287 { /* Preset swatches */ }
288 <div className="bp-color-picker__presets">
289 { PRESETS.map( ( hex ) => (
290 <button key={ hex } type="button"
291 className="bp-color-picker__preset"
292 style={ { background: hex } }
293 onClick={ () => applyHex( hex ) }
294 />
295 ) ) }
296 </div>
297
298 { /* Actions */ }
299 <div className="bp-color-picker__actions">
300 <button type="button" className="bp-color-picker__action-btn"
301 onClick={ () => applyHex( defaultValue || '#000000' ) }
302 >
303 { __( 'Reset', 'better-payment' ) }
304 </button>
305 <button type="button" className="bp-color-picker__action-btn"
306 onClick={ onClose }
307 >
308 { __( 'Close', 'better-payment' ) }
309 </button>
310 </div>
311 </div>
312 );
313 }
314