PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
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.7, at blocks/star-rating/index.js

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