PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.6
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.6
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / star-rating / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.6, at blocks/star-rating/index.js

491 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 wp.domReady(function () {
2 var el = wp.element.createElement;
3 var Fragment = wp.element.Fragment;
4 var useState = wp.element.useState;
5 var useEffect = wp.element.useEffect;
6 var __ = wp.i18n.__;
7 var registerBlockType = wp.blocks.registerBlockType;
8 var InspectorControls = wp.blockEditor.InspectorControls;
9 var useBlockProps = wp.blockEditor.useBlockProps;
10 var RichText = wp.blockEditor.RichText;
11 var PanelBody = wp.components.PanelBody;
12 var ColorPicker = wp.components.ColorPicker;
13 var Popover = wp.components.Popover;
14 var Button = wp.components.Button;
15 var ToggleControl = wp.components.ToggleControl;
16 var RangeControl = wp.components.RangeControl;
17 var SelectControl = wp.components.SelectControl;
18 var TextControl = wp.components.TextControl;
19
20 // ── SVG shape paths ─────────────────────────────────────────────────────────
21 var SHAPES = {
22 star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
23 heart: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
24 diamond:'M12 2l9 10-9 10L3 12z',
25 thumb: 'M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z'
26 };
27
28 // ── Generate unique block ID ─────────────────────────────────────────────────
29 function generateId() {
30 return 'sr' + Math.random().toString(36).substr(2, 8);
31 }
32
33 // ── Build a single star SVG ──────────────────────────────────────────────────
34 // fill: 'full' | 'half' | 'empty'
35 // clipId: unique string for the <clipPath id>
36 function buildStarSVG(fill, shape, filledColor, emptyColor, clipId) {
37 var filled = filledColor || '#f59e0b';
38 var empty = emptyColor || '#e2e8f0';
39 var svgProps = {
40 viewBox: '0 0 24 24',
41 xmlns: 'http://www.w3.org/2000/svg',
42 width: '100%',
43 height: '100%',
44 style: { display: 'block', overflow: 'visible' }
45 };
46
47 function makeShape(tag, extraProps) {
48 if (tag === 'circle') {
49 return el('circle', Object.assign({ cx: '12', cy: '12', r: '10' }, extraProps));
50 }
51 return el('path', Object.assign({ d: SHAPES[shape] || SHAPES.star }, extraProps));
52 }
53
54 var useCircle = shape === 'circle';
55 var shapeTag = useCircle ? 'circle' : 'path';
56
57 if (fill === 'full') {
58 return el('svg', svgProps, makeShape(shapeTag, { fill: filled }));
59 }
60 if (fill === 'empty') {
61 return el('svg', svgProps, makeShape(shapeTag, { fill: empty }));
62 }
63 // half — left 50% filled, right 50% empty, using clipPath
64 return el('svg', svgProps,
65 el('defs', {},
66 el('clipPath', { id: clipId },
67 el('rect', { x: '0', y: '0', width: '12', height: '24' })
68 )
69 ),
70 makeShape(shapeTag, { key: 'bg', fill: empty }),
71 makeShape(shapeTag, { key: 'fg', fill: filled, 'clipPath': 'url(#' + clipId + ')' })
72 );
73 }
74
75 // ── Build star row elements ──────────────────────────────────────────────────
76 function buildStars(rating, maxStars, shape, filledColor, emptyColor, blockId) {
77 var stars = [];
78 for (var i = 1; i <= maxStars; i++) {
79 var fill;
80 if (rating >= i) {
81 fill = 'full';
82 } else if (rating >= i - 0.5) {
83 fill = 'half';
84 } else {
85 fill = 'empty';
86 }
87 var clipId = 'bkbg-sr-' + blockId + '-' + i;
88 stars.push(el('span', {
89 key: i,
90 className: 'bkbg-sr-star',
91 'aria-hidden': 'true'
92 }, buildStarSVG(fill, shape, filledColor, emptyColor, clipId)));
93 }
94 return stars;
95 }
96
97 // ── Format numeric label ────────────────────────────────────────────────────
98 function formatNumeric(template, rating, max) {
99 return (template || '{rating} out of {max}')
100 .replace('{rating}', rating)
101 .replace('{max}', max);
102 }
103
104 registerBlockType('blockenberg/star-rating', {
105 title: __('Star Rating', 'blockenberg'),
106 icon: 'star-filled',
107 category: 'blockenberg',
108 description: __('Static visual star rating with optional label and numeric score.', 'blockenberg'),
109
110 edit: function (props) {
111 var a = props.attributes;
112 var setAttributes = props.setAttributes;
113
114 // ── Generate blockId on first mount ────────────────────────────────
115 useEffect(function () {
116 if (!a.blockId) {
117 setAttributes({ blockId: generateId() });
118 }
119 }, []);
120
121 // ── Hover state for interactive star picker ────────────────────────
122 var hoverState = useState(null);
123 var hoverRating = hoverState[0];
124 var setHoverRating = hoverState[1];
125
126 // ── Color picker state ─────────────────────────────────────────────
127 var openColorKeyState = useState(null);
128 var openColorKey = openColorKeyState[0];
129 var setOpenColorKey = openColorKeyState[1];
130
131 function renderColorControl(key, label, value, onChange) {
132 var isOpen = openColorKey === key;
133 return el('div', { key: key, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' } },
134 el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label),
135 el('div', { style: { position: 'relative', flexShrink: 0 } },
136 el('button', {
137 type: 'button',
138 title: value || 'none',
139 onClick: function () { setOpenColorKey(isOpen ? null : key); },
140 style: {
141 width: '28px', height: '28px', borderRadius: '4px',
142 border: isOpen ? '2px solid #007cba' : '2px solid #ddd',
143 cursor: 'pointer', padding: 0, display: 'block',
144 background: value || '#ffffff', flexShrink: 0
145 }
146 }),
147 isOpen && el(Popover, {
148 position: 'bottom left',
149 onClose: function () { setOpenColorKey(null); }
150 },
151 el('div', {
152 style: { padding: '8px' },
153 onMouseDown: function (e) { e.stopPropagation(); }
154 },
155 el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' } },
156 el('strong', { style: { fontSize: '12px' } }, label),
157 el(Button, { icon: 'no-alt', isSmall: true, onClick: function () { setOpenColorKey(null); } })
158 ),
159 el(ColorPicker, {
160 color: value,
161 enableAlpha: true,
162 onChange: function (c) { onChange(c); }
163 })
164 )
165 )
166 )
167 );
168 }
169
170 // ── Option lists ───────────────────────────────────────────────────
171 var starShapeOptions = [
172 { label: __('Star', 'blockenberg'), value: 'star' },
173 { label: __('Heart', 'blockenberg'), value: 'heart' },
174 { label: __('Diamond', 'blockenberg'), value: 'diamond' },
175 { label: __('Thumbs Up', 'blockenberg'), value: 'thumb' },
176 { label: __('Circle', 'blockenberg'), value: 'circle' }
177 ];
178
179 var textAlignOptions = [
180 { label: __('Left', 'blockenberg'), value: 'left' },
181 { label: __('Center', 'blockenberg'), value: 'center' },
182 { label: __('Right', 'blockenberg'), value: 'right' }
183 ];
184
185 var labelPositionOptions = [
186 { label: __('Below stars', 'blockenberg'), value: 'below' },
187 { label: __('Above stars', 'blockenberg'), value: 'above' },
188 { label: __('Left of stars', 'blockenberg'), value: 'left' },
189 { label: __('Right of stars', 'blockenberg'), value: 'right' }
190 ];
191
192 var fontWeightOptions = [
193 { label: '300 — Light', value: 300 },
194 { label: '400 — Regular', value: 400 },
195 { label: '500 — Medium', value: 500 },
196 { label: '600 — Semi Bold', value: 600 },
197 { label: '700 — Bold', value: 700 },
198 { label: '800 — Extra Bold', value: 800 }
199 ];
200
201 // Display rating: use hover preview if hovering
202 var displayRating = hoverRating !== null ? hoverRating : a.rating;
203 var blockId = a.blockId || 'preview';
204
205 // ── Interactive star picker (canvas) ───────────────────────────────
206 function makeInteractiveStar(i) {
207 var isFilled = displayRating >= i;
208 var isHalfFill = displayRating >= i - 0.5 && displayRating < i;
209 var fill = isFilled ? 'full' : (isHalfFill ? 'half' : 'empty');
210 var clipId = 'bkbg-sr-edit-' + blockId + '-' + i;
211 return el('span', {
212 key: 'es-' + i,
213 className: 'bkbg-sr-star bkbg-sr-star--interactive',
214 title: i + ' ' + __('stars', 'blockenberg'),
215 onMouseEnter: function () { setHoverRating(i); },
216 onMouseLeave: function () { setHoverRating(null); },
217 onClick: function () { setAttributes({ rating: i }); },
218 style: { cursor: 'pointer' }
219 }, buildStarSVG(fill, a.starShape, a.filledColor, a.emptyColor, clipId));
220 }
221
222 var interactiveStars = [];
223 for (var i = 1; i <= a.maxStars; i++) {
224 interactiveStars.push(makeInteractiveStar(i));
225 }
226
227 // ── CSS vars ──────────────────────────────────────────────────────
228 var wrapStyle = {
229 '--bkbg-sr-star-size': a.starSize + 'px',
230 '--bkbg-sr-star-gap': a.starGap + 'px',
231 '--bkbg-sr-label-size': a.labelSize + 'px',
232 '--bkbg-sr-label-weight': a.labelWeight,
233 '--bkbg-sr-label-color': a.labelColor,
234 '--bkbg-sr-label-lh': a.labelLH,
235 '--bkbg-sr-label-spacing': a.labelSpacing + 'px',
236 '--bkbg-sr-num-size': a.numericSize + 'px',
237 '--bkbg-sr-num-weight': a.numericWeight,
238 '--bkbg-sr-num-color': a.numericColor,
239 '--bkbg-sr-num-spacing': a.numericSpacing + 'px'
240 };
241
242 // ── Inspector Controls ─────────────────────────────────────────────
243 var inspector = el(InspectorControls, {},
244
245 // ── Stars ─────────────────────────────────────────────────────
246 el(PanelBody, { title: __('Stars', 'blockenberg'), initialOpen: true },
247 el('p', { style: { margin: '0 0 8px', fontSize: '12px', color: '#757575' } },
248 __('Click a star in the block to quickly set the rating.', 'blockenberg')
249 ),
250 el(RangeControl, {
251 label: __('Rating', 'blockenberg'),
252 value: a.rating,
253 min: 0,
254 max: a.maxStars,
255 step: 0.5,
256 onChange: function (v) { setAttributes({ rating: v }); }
257 }),
258 el(RangeControl, {
259 label: __('Max Stars', 'blockenberg'),
260 value: a.maxStars,
261 min: 1,
262 max: 10,
263 onChange: function (v) {
264 var newMax = v;
265 setAttributes({ maxStars: newMax, rating: Math.min(a.rating, newMax) });
266 }
267 }),
268 el(SelectControl, {
269 label: __('Shape', 'blockenberg'),
270 value: a.starShape,
271 options: starShapeOptions,
272 onChange: function (v) { setAttributes({ starShape: v }); }
273 }),
274 el(RangeControl, {
275 label: __('Size (px)', 'blockenberg'),
276 value: a.starSize,
277 min: 12,
278 max: 80,
279 onChange: function (v) { setAttributes({ starSize: v }); }
280 }),
281 el(RangeControl, {
282 label: __('Gap between stars (px)', 'blockenberg'),
283 value: a.starGap,
284 min: 0,
285 max: 24,
286 onChange: function (v) { setAttributes({ starGap: v }); }
287 })
288 ),
289
290 // ── Layout ────────────────────────────────────────────────────
291 el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: false },
292 el(SelectControl, {
293 label: __('Alignment', 'blockenberg'),
294 value: a.textAlign,
295 options: textAlignOptions,
296 onChange: function (v) { setAttributes({ textAlign: v }); }
297 }),
298 el(ToggleControl, {
299 label: __('Show Label', 'blockenberg'),
300 checked: a.showLabel,
301 __nextHasNoMarginBottom: true,
302 onChange: function (v) { setAttributes({ showLabel: v }); }
303 }),
304 a.showLabel && el(SelectControl, {
305 label: __('Label Position', 'blockenberg'),
306 value: a.labelPosition,
307 options: labelPositionOptions,
308 onChange: function (v) { setAttributes({ labelPosition: v }); }
309 }),
310 el('hr', {}),
311 el(ToggleControl, {
312 label: __('Show Numeric Score', 'blockenberg'),
313 checked: a.showNumeric,
314 __nextHasNoMarginBottom: true,
315 onChange: function (v) { setAttributes({ showNumeric: v }); }
316 }),
317 a.showNumeric && el(TextControl, {
318 label: __('Numeric Template', 'blockenberg'),
319 value: a.numericTemplate,
320 help: __('Use {rating} and {max} as placeholders.', 'blockenberg'),
321 onChange: function (v) { setAttributes({ numericTemplate: v }); }
322 })
323 ),
324
325 // ── Typography ────────────────────────────────────────────────
326 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
327 a.showLabel && el(Fragment, {},
328 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } }, __('Label', 'blockenberg')),
329 el(RangeControl, {
330 label: __('Font Size', 'blockenberg'),
331 value: a.labelSize,
332 min: 10,
333 max: 36,
334 onChange: function (v) { setAttributes({ labelSize: v }); }
335 }),
336 el(SelectControl, {
337 label: __('Font Weight', 'blockenberg'),
338 value: a.labelWeight,
339 options: fontWeightOptions,
340 onChange: function (v) { setAttributes({ labelWeight: parseInt(v, 10) }); }
341 }),
342 el(RangeControl, {
343 label: __('Line Height', 'blockenberg'),
344 value: a.labelLH,
345 min: 1,
346 max: 2.5,
347 step: 0.05,
348 onChange: function (v) { setAttributes({ labelLH: v }); }
349 }),
350 el(RangeControl, {
351 label: __('Spacing from stars', 'blockenberg'),
352 value: a.labelSpacing,
353 min: 0,
354 max: 40,
355 onChange: function (v) { setAttributes({ labelSpacing: v }); }
356 })
357 ),
358 a.showNumeric && el(Fragment, {},
359 el('hr', {}),
360 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } }, __('Numeric Score', 'blockenberg')),
361 el(RangeControl, {
362 label: __('Font Size', 'blockenberg'),
363 value: a.numericSize,
364 min: 10,
365 max: 28,
366 onChange: function (v) { setAttributes({ numericSize: v }); }
367 }),
368 el(SelectControl, {
369 label: __('Font Weight', 'blockenberg'),
370 value: a.numericWeight,
371 options: fontWeightOptions,
372 onChange: function (v) { setAttributes({ numericWeight: parseInt(v, 10) }); }
373 }),
374 el(RangeControl, {
375 label: __('Spacing from stars', 'blockenberg'),
376 value: a.numericSpacing,
377 min: 0,
378 max: 40,
379 onChange: function (v) { setAttributes({ numericSpacing: v }); }
380 })
381 )
382 ),
383
384 // ── Colors ────────────────────────────────────────────────────
385 el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false },
386 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } }, __('Stars', 'blockenberg')),
387 renderColorControl('filledColor', __('Filled star', 'blockenberg'), a.filledColor, function (c) { setAttributes({ filledColor: c }); }),
388 renderColorControl('emptyColor', __('Empty star', 'blockenberg'), a.emptyColor, function (c) { setAttributes({ emptyColor: c }); }),
389 a.showLabel && el(Fragment, {},
390 el('hr', {}),
391 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } }, __('Label', 'blockenberg')),
392 renderColorControl('labelColor', __('Label text', 'blockenberg'), a.labelColor, function (c) { setAttributes({ labelColor: c }); })
393 ),
394 a.showNumeric && el(Fragment, {},
395 el('hr', {}),
396 el('p', { style: { margin: '4px 0 6px', fontWeight: 600, fontSize: '11px', textTransform: 'uppercase', color: '#888' } }, __('Numeric', 'blockenberg')),
397 renderColorControl('numericColor', __('Numeric text', 'blockenberg'), a.numericColor, function (c) { setAttributes({ numericColor: c }); })
398 )
399 )
400 );
401
402 // ── Edit render ────────────────────────────────────────────────────────
403 var blockProps = useBlockProps({
404 className: 'bkbg-editor-wrap',
405 'data-block-label': 'Star Rating'
406 });
407
408 var numericEl = a.showNumeric && el('span', { className: 'bkbg-sr-numeric' },
409 formatNumeric(a.numericTemplate, a.rating, a.maxStars)
410 );
411
412 var starsRow = el('div', {
413 className: 'bkbg-sr-stars',
414 'aria-label': a.rating + ' ' + __('out of', 'blockenberg') + ' ' + a.maxStars + ' ' + __('stars', 'blockenberg'),
415 title: __('Click a star to set rating', 'blockenberg')
416 }, interactiveStars, numericEl);
417
418 var labelEl = a.showLabel && el(RichText, {
419 tagName: 'p',
420 className: 'bkbg-sr-label',
421 value: a.label,
422 onChange: function (v) { setAttributes({ label: v }); },
423 placeholder: __('Label text…', 'blockenberg'),
424 allowedFormats: ['core/bold', 'core/italic', 'core/text-color', 'core/link']
425 });
426
427 // DOM order is always [starsRow, labelEl].
428 // CSS flex-direction (column-reverse / row) handles visual reordering.
429 var children = [starsRow, a.showLabel ? labelEl : null];
430
431 return el('div', blockProps,
432 inspector,
433 el('div', Object.assign({ className: 'bkbg-sr-wrap', style: wrapStyle }, {
434 'data-text-align': a.textAlign,
435 'data-label-pos': a.labelPosition,
436 'data-show-label': a.showLabel ? 'true' : 'false'
437 }), children)
438 );
439 },
440
441 // ── Save ─────────────────────────────────────────────────────────────────
442 save: function (props) {
443 var a = props.attributes;
444 var RichTextContent = wp.blockEditor.RichText.Content;
445 var blockId = a.blockId || 'sr0';
446
447 var wrapStyle = {
448 '--bkbg-sr-star-size': a.starSize + 'px',
449 '--bkbg-sr-star-gap': a.starGap + 'px',
450 '--bkbg-sr-label-size': a.labelSize + 'px',
451 '--bkbg-sr-label-weight': a.labelWeight,
452 '--bkbg-sr-label-color': a.labelColor,
453 '--bkbg-sr-label-lh': a.labelLH,
454 '--bkbg-sr-label-spacing': a.labelSpacing + 'px',
455 '--bkbg-sr-num-size': a.numericSize + 'px',
456 '--bkbg-sr-num-weight': a.numericWeight,
457 '--bkbg-sr-num-color': a.numericColor,
458 '--bkbg-sr-num-spacing': a.numericSpacing + 'px'
459 };
460
461 var starEls = buildStars(a.rating, a.maxStars, a.starShape, a.filledColor, a.emptyColor, blockId);
462
463 var numericEl = a.showNumeric && el('span', { className: 'bkbg-sr-numeric' },
464 formatNumeric(a.numericTemplate, a.rating, a.maxStars)
465 );
466
467 var starsRow = el('div', {
468 className: 'bkbg-sr-stars',
469 role: 'img',
470 'aria-label': a.rating + ' out of ' + a.maxStars + ' stars'
471 }, starEls, numericEl);
472
473 var labelEl = a.showLabel && el(RichTextContent, {
474 tagName: 'p',
475 className: 'bkbg-sr-label',
476 value: a.label
477 });
478
479 // DOM order is always [starsRow, labelEl].
480 // CSS flex-direction (column-reverse / row) handles visual reordering.
481 var children = [starsRow, a.showLabel ? labelEl : null];
482
483 return el('div', Object.assign({ className: 'bkbg-sr-wrap', style: wrapStyle }, {
484 'data-text-align': a.textAlign,
485 'data-label-pos': a.labelPosition,
486 'data-show-label': a.showLabel ? 'true' : 'false'
487 }), children);
488 }
489 });
490 });
491